《ES6標準入門》:函式的擴充套件
目錄
1.函式引數預設值
1.1 基本用法
ES6之前不能指定預設引數:
let a = 0
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World
sfasdf
ES6支援指定預設引數:
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello' ) // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
好處:
1.程式碼更加簡潔;
2.語義化更強,易於閱讀;
3.容錯更高,引數不寫也不會報錯。
錯誤用法:
1.引數變數是預設宣告的,所以不能用let
或const
再次宣告。
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
2.使用引數預設值時,函式不能有同名引數。
function foo(x, x, y = 1) {
// ...
}
// SyntaxError: Duplicate parameter name not allowed in this context
1.2 與解構賦值預設值結合使用
基本場景
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 2}) // 1, 2
foo() // TypeError: Cannot read property 'x' of undefined
易混點
// 寫法一
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 寫法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
上面兩種寫法都對函式的引數設定了預設值,區別是寫法一函式引數的預設值是空物件,但是設定了物件解構賦值的預設值;寫法二函式引數的預設值是一個有具體屬性的物件,但是沒有設定物件解構賦值的預設值。
// 函式沒有引數的情況
m1() // [0, 0]
m2() // [0, 0]
// x和y都有值的情況
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]
// x有值,y無值的情況
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]
// x和y都無值的情況
m1({}) // [0, 0];
m2({}) // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
1.3 引數預設值位置
通常情況下,定義了預設值的引數,應該是函式的尾引數。因為這樣比較容易看出來,到底省略了哪些引數。如果非尾部的引數設定預設值,實際上這個引數是沒法省略的。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 報錯
f(undefined, 1) // [1, 1]
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 報錯
f(1, undefined, 2) // [1, 5, 2]
上面程式碼中,有預設值的引數都不是尾引數。這時,無法只省略該引數,而不省略它後面的引數,除非顯式輸入undefined
。
如果傳入undefined
,將觸發該引數等於預設值,null
則沒有這個效果。
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null)
// 5 null
上面程式碼中,x
引數對應undefined
,結果觸發了預設值,y
引數等於null
,就沒有觸發預設值。
1.4 函式的 length 屬性
指定了預設值以後,函式的length
屬性,將返回沒有指定預設值的引數個數。也就是說,指定了預設值後,length
屬性將失真。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
1.5 作用域
區域性優先於全域性
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
預設賦值前未宣告情況
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
也就是用變數給引數賦值前得先宣告。
關於函式作為引數作用域
和之前遵循一樣的規則:
let foo = 'outer';
function bar(func = x => foo) {
let foo = 'inner';
console.log(func());
}
bar(); // outer
2.rest引數
引數形式 :
...變數名
其實就是把引數轉化成了一個數組:
function add(...values) {
console.log(values)
}
add(2, 5, 3) // [2,3,5]
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
注意 :
rest 引數之後不能再有其他引數(即只能是最後一個引數),否則會報錯。
// 報錯
function f(a, ...b, c) {
// ...
}
函式的length
屬性,不包括 rest 引數。
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
3.擴充套件運算子
形式 :
...
好比 rest 引數的逆運算,將一個數組轉為用逗號分隔的引數序列。
基本用法 :
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
該運算子主要用於函式呼叫。
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
var numbers = [4, 38];
add(...numbers) // 42
替代陣列的apply方法
由於擴充套件運算子可以展開陣列,所以不再需要apply
方法,將陣列轉為函式的引數了。
// ES5的寫法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的寫法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f(...args);
下面是擴充套件運算子取代apply
方法的一個實際的例子,應用Math.max
方法,簡化求出一個數組最大元素的寫法。
// ES5的寫法
Math.max.apply(null, [14, 3, 77])
// ES6的寫法
Math.max(...[14, 3, 77])
// 等同於
Math.max(14, 3, 77);
上面程式碼表示,由於JavaScript不提供求陣列最大元素的函式,所以只能套用Math.max
函式,將陣列轉為一個引數序列,然後求最大值。有了擴充套件運算子以後,就可以直接用Math.max
了。
另一個例子是通過push
函式,將一個數組新增到另一個數組的尾部。
// ES5的寫法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
// ES6的寫法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);
上面程式碼的ES5寫法中,push
方法的引數不能是陣列,所以只好通過apply
方法變通使用push
方法。有了擴充套件運算子,就可以直接將陣列傳入push
方法。
下面是另外一個例子。
// ES5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
// ES6
new Date(...[2015, 1, 1]);
擴充套件運算子的應用
(1)合併陣列
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]
var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];
// ES5的合併陣列
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6的合併陣列
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
(2)與解構賦值結合來生成陣列
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
如果將擴充套件運算子用於陣列賦值,只能放在引數的最後一位,否則會報錯。
const [...butLast, last] = [1, 2, 3, 4, 5];
// 報錯
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 報錯
從 ES5 開始,函式內部可以設定為嚴格模式。
function doSomething(a, b) {
'use strict';
// code
}
4.嚴格模式
ES2016 做了一點修改,規定只要函式引數使用了預設值、解構賦值、或者擴充套件運算子,那麼函式內部就不能顯式設定為嚴格模式,否則會報錯。
// 報錯
function doSomething(a, b = a) {
'use strict';
// code
}
// 報錯
const doSomething = function ({a, b}) {
'use strict';
// code
};
// 報錯
const doSomething = (...a) => {
'use strict';
// code
};
const obj = {
// 報錯
doSomething({a, b}) {
'use strict';
// code
}
};
5.箭頭函式
5.1 基本用法
箭頭函式是ES6中被應用的最廣泛的語法之一了,關鍵在於其形式簡單:
var f = v => v;
上面的箭頭函式等同於:
var f = function(v) {
return v;
};
如果箭頭函式不需要引數或需要多個引數,就使用一個圓括號代表引數部分。
var f = () => 5;
// 等同於
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同於
var sum = function(num1, num2) {
return num1 + num2;
};
箭頭函式的一個用處是簡化回撥函式。
// 正常函式寫法
[1,2,3].map(function (x) {
return x * x;
});
// 箭頭函式寫法
[1,2,3].map(x => x * x);
下面是 rest 引數與箭頭函式結合的例子。
const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]
5.2 注意點
this
物件的指向是可變的,但是在箭頭函式中,它是固定的。
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
5.3 巢狀的箭頭函式
箭頭函式內部,還可以再使用箭頭函式。下面是一個 ES5 語法的多重巢狀函式。
function insert(value) {
return {into: function (array) {
return {after: function (afterValue) {
array.splice(array.indexOf(afterValue) + 1, 0, value);
return array;
}};
}};
}
insert(2).into([1, 3]).after(1); //[1, 2, 3]
上面這個函式,可以使用箭頭函式改寫。
let insert = (value) => ({into: (array) => ({after: (afterValue) => {
array.splice(array.indexOf(afterValue) + 1, 0, value);
return array;
}})});
insert(2).into([1, 3]).after(1); //[1, 2, 3]
6.尾呼叫優化
6.1 什麼是尾呼叫?
尾呼叫(Tail Call)是函數語言程式設計的一個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函式的最後一步是呼叫另一個函式。
function f(x){
return g(x);
}
上面程式碼中,函式f
的最後一步是呼叫函式g
,這就叫尾呼叫。
以下三種情況,都不屬於尾呼叫。
// 情況一
function f(x){
let y = g(x);
return y;
}
// 情況二
function f(x){
return g(x) + 1;
}
// 情況三
function f(x){
g(x);
}
上面程式碼中,情況一是呼叫函式g
之後,還有賦值操作,所以不屬於尾呼叫,即使語義完全一樣。情況二也屬於呼叫後還有操作,即使寫在一行內。情況三等同於下面的程式碼。
function f(x){
g(x);
return undefined;
}
尾呼叫不一定出現在函式尾部,只要是最後一步操作即可。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
上面程式碼中,函式m
和n
都屬於尾呼叫,因為它們都是函式f
的最後一步操作。
6.2 尾呼叫優化
尾呼叫之所以與其他呼叫不同,就在於它的特殊的呼叫位置。
我們知道,函式呼叫會在記憶體形成一個“呼叫記錄”,又稱“呼叫幀”(call frame),儲存呼叫位置和內部變數等資訊。如果在函式A
的內部呼叫函式B
,那麼在A
的呼叫幀上方,還會形成一個B
的呼叫幀。等到B
執行結束,將結果返回到A
,B
的呼叫幀才會消失。如果函式B
內部還呼叫函式C
,那就還有一個C
的呼叫幀,以此類推。所有的呼叫幀,就形成一個“呼叫棧”(call stack)。
尾呼叫由於是函式的最後一步操作,所以不需要保留外層函式的呼叫幀,因為呼叫位置、內部變數等資訊都不會再用到了,只要直接用內層函式的呼叫幀,取代外層函式的呼叫幀就可以了。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同於
function f() {
return g(3);
}
f();
// 等同於
g(3);
上面程式碼中,如果函式g
不是尾呼叫,函式f
就需要儲存內部變數m
和n
的值、g
的呼叫位置等資訊。但由於呼叫g
之後,函式f
就結束了,所以執行到最後一步,完全可以刪除f(x)
的呼叫幀,只保留g(3)
的呼叫幀。
這就叫做“尾呼叫優化”(Tail call optimization),即只保留內層函式的呼叫幀。如果所有函式都是尾呼叫,那麼完全可以做到每次執行時,呼叫幀只有一項,這將大大節省記憶體。這就是“尾呼叫優化”的意義。
注意,只有不再用到外層函式的內部變數,內層函式的呼叫幀才會取代外層函式的呼叫幀,否則就無法進行“尾呼叫優化”。
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}
上面的函式不會進行尾呼叫優化,因為內層函式inner
用到了外層函式addOne
的內部變數one
。
6.3 尾遞迴
函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。
遞迴非常耗費記憶體,因為需要同時儲存成千上百個呼叫幀,很容易發生“棧溢位”錯誤(stack overflow)。但對於尾遞迴來說,由於只存在一個呼叫幀,所以永遠不會發生“棧溢位”錯誤。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
上面程式碼是一個階乘函式,計算n
的階乘,最多需要儲存n
個呼叫記錄,複雜度 O(n) 。
如果改寫成尾遞迴,只保留一個呼叫記錄,複雜度 O(1) 。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
還有一個比較著名的例子,就是計算 Fibonacci 數列,也能充分說明尾遞迴優化的重要性。
非尾遞迴的 Fibonacci 數列實現如下。
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 堆疊溢位
Fibonacci(500) // 堆疊溢位
尾遞迴優化過的 Fibonacci 數列實現如下。
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
由此可見,“尾呼叫優化”對遞迴操作意義重大,所以一些函數語言程式設計語言將其寫入了語言規格。ES6 是如此,第一次明確規定,所有 ECMAScript 的實現,都必須部署“尾呼叫優化”。這就是說,ES6 中只要使用尾遞迴,就不會發生棧溢位,相對節省記憶體。
6.4 遞迴函式的改寫
尾遞迴的實現,往往需要改寫遞迴函式,確保最後一步只調用自身。做到這一點的方法,就是把所有用到的內部變數改寫成函式的引數。比如上面的例子,階乘函式 factorial 需要用到一箇中間變數total
,那就把這個中間變數改寫成函式的引數。這樣做的缺點就是不太直觀,第一眼很難看出來,為什麼計算5
的階乘,需要傳入兩個引數5
和1
?
兩個方法可以解決這個問題。方法一是在尾遞迴函式之外,再提供一個正常形式的函式。
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
function factorial(n) {
return tailFactorial(n, 1);
}
factorial(5) // 120
上面程式碼通過一個正常形式的階乘函式factorial
,呼叫尾遞迴函式tailFactorial
,看起來就正常多了。
函數語言程式設計有一個概念,叫做柯里化(currying),意思是將多引數的函式轉換成單引數的形式。這裡也可以使用柯里化。
function currying(fn, n) {
return function (m) {
return fn.call(this, m, n);
};
}
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
const factorial = currying(tailFactorial, 1);
factorial(5) // 120
上面程式碼通過柯里化,將尾遞迴函式tailFactorial
變為只接受一個引數的factorial
。
第二種方法就簡單多了,就是採用 ES6 的函式預設值。
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5) // 120
上面程式碼中,引數total
有預設值1
,所以呼叫時不用提供這個值。
總結一下,遞迴本質上是一種迴圈操作。純粹的函數語言程式設計語言沒有迴圈操作命令,所有的迴圈都用遞迴實現,這就是為什麼尾遞迴對這些語言極其重要。對於其他支援“尾呼叫優化”的語言(比如Lua,ES6),只需要知道迴圈可以用遞迴代替,而一旦使用遞迴,就最好使用尾遞迴。
相關推薦
《ES6標準入門》:函式的擴充套件
目錄 1.函式引數預設值 1.1 基本用法 ES6之前不能指定預設引數: let a = 0 function log(x, y) { y = y || 'World'; console.log(x, y); } l
ECMAScript6(ES6)標準之函式擴充套件特性箭頭函式、Rest引數及展開操作符
ES6擴充套件了很多語法糖語法 其中對於函式我們又可以使用一種叫做“箭頭函式”的寫法 同時引入了Rest引數 利用“…”可以獲取多餘引數 這樣就我們就不要使用arguments物件了 下面我來詳細地談一談 函式預設引數 ES6沒有出現之前 面
《ES6標準入門》29~48Page 字符串拓展 正則拓展
har 字節 其中 logs 屬性表 regex fff 不能 包含 1.字符串的拓展 ES3允許使用類似\u0061這樣的形式來表示字符,其中的數字是Unicode-8編碼。 但如果超出\uffff的字符,必須使用雙字節的形式表達,例如 \uD842\uDFB7。 在ES
《ES6標準入門》49~68Page 數值的拓展 數組的拓展
() 給定 結果 格式 int 指數 undefine define 可能 1.數值拓展 ES6提供的二進制和八進制表示法分別是二進制: 0B111110111(0b111110111) 八進制: 0O767(0o767) ES6提供了新的Number.isFinite()
ES6標準入門 第二章:塊級作用域 以及 let和const命令
函數聲明 web 頂部 16px 地址 value length window對象 成功 一、塊級作用域 1、為什麽需要塊級作用域? ES5中只有全局作用域和函數作用域,帶來很多不合理的場景。 (1)內層變量可能會覆蓋外層變量; var tem = ne
ES6標準入門 第四章:字符串的擴展
固定 缺陷 長度 需要 允許 實例對象 poi turn har 1、字符串的Unicode 表示法 JavaScript 允許采用 \uxxxx 表示一個字符,其中 xxxx 表示字符的碼點。 "\u0061" // "a" ES5中的缺陷: 以上表示
ES6標準入門(第三版)學習筆記(1)
ES6宣告變數的六種方法 ES5只有兩種 var,function命令 ES6新增了let,const,class,import命令 驗證var與let用法上的不同 var a = []; for (var i = 0; i < 10; i++){
ES6標準入門之變數的解構賦值簡單解說
首先我們來看一看解構的概念,在ES6標準下,允許按照一定模式從陣列和物件中提取值,然後對變數進行賦值,這被稱作解構,簡而言之粗糙的理解就是變相賦值。 解構賦值的規則是,只要等號右邊的值不是物件或者陣列,就先將其轉為物件。 一、陣列的結構賦值 以前為變數賦值只能直接指定。而ES6允許從陣列
ES6標準入門之字串的拓展講解
在開始講解ES6中字串拓展之前,我們先來看一下ES5中字串的一些方法。 獲取字串長度 str.length 分割字串 str.split() 拼接字串 str1+str2 或 str1.concat(str2) 替換字串 str.replace(“玩遊戲”,”好好學習”)
ES6標準入門之正則表示式的拓展
所謂正則表示式,又稱規則表示式。(英語:Regular Expression,在程式碼中常簡寫為regex、regexp或RE),電腦科學的一個概念。正則表示式通常被用來檢索、替換那些符合某個模式(規則)的文字。在之前使用基於Jquery庫開發專案的時候,用的正則表示式最多的就是一些輸入框的檢驗,比如檢驗
《ES6標準入門》(九)之Class
昨天,360面試官問了個問題,說:用ES5怎麼實現ES6中的class? 因為沒有看過class,就說不了解,結果回來一看,這不就是ES5怎麼建立物件嗎???我寫了那麼多,看了那麼多,就這麼不會的完事的,哎。。。今天,來總結一下ES6中的Class 極客學院講的很詳細,參
《es6標準入門》讀書筆記-第一章 ECMAScript 6簡介
以下內容使用的書籍為《es6標準入門-第2版》,阮一峰著,如有需要請購買正版 本文僅為個人讀書筆記,如有不詳之處請查閱原文 執行環境 node環境的檢查 檢視node對es6的支援狀況 $ node --v8-optio
《es6標準入門》 阮一峰
2 let和const命令 2.1 let命令 2.1.1 基本用法 2.1.2 不存在變數提升 2.1.3 暫時性死區 2.1.4 不允許重複宣告 2.2 塊級作用域 2.2.1 為什麼需要塊
js -- ES6(一)-- 簡介(根據阮一峰ES6標準入門整理)
目前正在學習ES6,根據阮一峰的ES6入門2,學到哪更新到哪裡,都是基本的知識,複雜的目前還不會,涉及的程式碼都是親自執行過的,若發現錯誤請指正。 ES6 提供了許多新特性,但是並不是所有的瀏
《ES6 標準入門》讀書筆記
過年在家閒著沒事,來看看ES6,選了阮一峰大大的《ES6 標準入門》這本書,瞭解一下新的js規範。這裡做一下讀書筆記。 ECMAScript 6 須知 目前各大瀏覽器的自新版本應該都支援ES6了,並且Node.js對ES6的支援度比瀏覽器還高,通過Nod
《es6標準入門》知識整理(4)- Reflect
昨天,我做了 es6 中的新物件 Proxy 的相關的知識整理,現在我會整理一下 es6 中另外一個新的內建物件:Reflect。
es6 入坑筆記(二)---函式擴充套件,箭頭函式,擴充套件運算子...
函式擴充套件 1.函式可以有預設值 function demo( a = 10,b ){} 2.函式可以使用解構 function demo( { a = 0,b = 0 } = {} ){ } 3.函式引數最後可以多一個逗號 function demo(a,b,
es6——函式擴充套件
1.形參設定預設值 es5 { function sum1(num1, num2) { num1 = num1 || 10;
es6 函式擴充套件,引數作用域和箭頭函式
函式的擴充套件 函式引數的預設值 基本用法 ES6 之前,不能直接為函式的引數指定預設值,只能採用變通的方法。 function log(x, y = 'World') { console.log(x, y); } log('Hello
js-ES6學習筆記-函式的擴充套件
1、ES6函式引數的預設值,直接寫在引數定義的後面。引數變數是預設宣告的,所以不能用let或const再次宣告。 function Point(x = 0, y = 0) { this.x = x; this.y = y; } var p = new Point(); p //