文章編寫參考 阮一峯《ECMAScript 6 入門》
1. 函數參數的默認值
1.1 基本用法
在ES6之前如果要給函數賦值一般採用以下這樣的方式
function fun(x, y) {
y = y || "Blue";
console.log(x, y);
}
fun("Hi") //Hi Blue
fun("Hi", "Lucky") //Hi Lucky
上面代碼中爲參數【y】指定了默認值,但是這樣的寫法在遇到【y】如果是布爾類型的值的時候,加入我爲【y】賦值爲false,那麼會發現沒法賦值成功。
爲了避免上面的問題,我們將上面的函數進行改寫
function fun(x, y) {
if (typeof y === 'undefined') {
y = y || "Blue";
}
console.log(x, y);
}
有了ES6之後就方便,我們可以直接在函數參數列表中進行參數默認值的定義
function fun(x, y = 'Blue') {
console.log(x, y);
}
fun("Hi") //Hi Blue
fun("Hi", "Lucky") //Hi Lucky
上面的例子看出ES6的寫法比ES5的簡潔了很多,而且不存在布爾值的問題,下面是另外一個【構造函數】的例子
function fun(x = 'Hi', y = 'Blue') {
this.x = x;
this.y = y;
}
var foo = new fun(); //{ x: 'Hi', y: 'Blue' }
【注意】:函數的參數是默認聲明的,所以在函數作用域中不能存在與參數相同名稱的變量聲明
function fun(x = 'Blue') {
let x = 'Crazy'; // error
const x = 'Jack'; // error
}
上面代碼片段中,x 爲參數變量,在該函數作用域中進行再次聲明引起報錯。
函數參數的默認值可以使用表達式,但是表達式是【惰性求值】的,也就是說在函數調用的時候重新計算默認值。
let x = 10;
function fun(r = x + 1) {
console.log(r);
}
fun(); //11
x = 100;
fun(); //101
上面代碼中函數默認值是一個【簡單表達式】,可以看出表達式在賦值的時候是在應用的時候才進行的。
let add = (x, y) => x + y;
let sub = (x, y) => x - y;
let fun = (r = add(1, 2)) => console.log(r);
fun(); //3
fun(sub(4, 2)); //2
上面代碼中函數默認值使用的是【函數賦值】,可以更加清晰的看出函數默認值的賦值時【惰性求值】的。
1.2 函數默認值與解構賦值結合使用
既然函數參數實際上也是一個變量聲明的過程,那麼函數默認值也可以使用【解構賦值】
let fun = ({ x, y = 'Blue' }) => console.log(x, y);
fun({}); //undefined Blue
fun({ x: "Hi" }); //Hi Blue
fun({ x: 'Hi', y: 'Crazy' }); //Hi Crazy
fun(); //// TypeError: Cannot read property 'x' of undefined
上面解構賦值代碼中,只有當fun參數是一個對象時,默認值的解構賦值纔會生效,最後一行代碼沒有傳參就報錯了。
我們看看對上面例子的改造
let fun = ({ x, y = 'Blue' } = {}) => console.log(x, y);
fun(); //undefined 'Blue'
上面代碼中函數未給予參數仍然能夠正常的運行。
【注意】看一看下面兩種寫法的差別,就明白上面兩個例子的差異在哪兒了
//寫法一
let fun = ({ x = 'Hi', y = 'Blue' } = {}) => console.log(x, y);
//寫法二
let fun1 = ({ x, y } = { x: 'Hi', y: 'Blue' }) => console.log(x, y);
上面兩種寫法都對函數參數設定了默認值,區別是寫法一函數參數的默認值是空對象,但是設置了對象解構賦值的默認值,寫法二函數參數的默認值是一個有具體屬性的對象,但是沒有設置對象解構賦值的默認值。
let fun = ({ x = 'Hi', y = 'Blue' } = {}) => console.log(x, y);
let fun1 = ({ x, y } = { x: 'Hi', y: 'Blue' }) => console.log(x, y);
//都沒有參數的情況
fun(); //Hi Blue
fun1(); //Hi Blue
//一樣參數的情況
fun({x: 'Hello', y: 'Crazy'}); //Hello Crazy
fun1({x: 'Hello', y: 'Crazy'}); //Hello Crazy
//缺失值得情況
fun({x: 'Hello'}); //Hello Blue
fun1({x: 'Hello'}); //Hello undefined
//都無值得情況
fun({}); //Hi Blue
fun1({}); //undefined undefined
看出區別了嗎?函數在傳入參數的時候,會替換原有=右邊的對象,如果我們將默認值放在=右邊的對象屬性中進行默認解構,那麼當函數傳入參數的時候這個對象就被替換掉了。
1.3 參數默認值的位置
通常情況下,定義了默認值的參數,應該是函數的尾參數。因爲這樣比較容易看出來,到底省略了哪些參數。如果非尾部的參數設置默認值,實際上這個參數是沒法省略的。
let fun = (x, y = 'Blue', z) => console.log(x, y, z);
fun(); //undefined 'Blue' undefined
fun('Hi', 'Crazy', 'Nice') //Hi Crazy Nice
f('Hi',, 'Nice') // 報錯
上面代碼中,有默認值的參數不是尾參數。這時,無法只省略該參數,而不省略它後面的參數,除非顯式輸入undefined。
let fun = (x, y = 'Blue', z) => console.log(x, y, z);
fun('Hi', undefined, 'Nice'); //Hi Blue Nice
上面代碼中y傳入undefined參數,根據解構賦值的規則會觸發默認值
1.4 函數的length屬性
函數的【length】屬性返回的是沒有默認值的參數個數,也就是說指定了默認值的參數就不會計入length屬性的計數中。
(x => x).length //1
((x='Blue') => x).length //0
((x, y, z = 'Blue') => x).length //2
上面代碼中第二段和第三段都設置了參數默認值,可以看出設置了默認值的參數不計入length計算。
((x = 'Blue', y, z) => x).length; //0
((x, y = 'Blue', z) => x).length; //1
((x, y, z = 'Blue') => x).length; //2
上面代碼中參數默認值的位置不同導致了【length】屬性的不同,其實是,【如果設置了默認值的參數不是尾參數,那麼length屬性也不再計入後面的參數了。】
1.5 作用域
一旦設置了參數的默認值,函數進行聲明初始化時,參數會形成一個單獨的作用域(context)。等到初始化結束,這個作用域就會消失。這種語法行爲,在不設置參數默認值時,是不會出現的。
let x = 'Blue';
let fun = (x, y = x) => console.log(x, y);
fun('Crazy'); //Crazy Crazy
上面代碼中,參數y的默認值等於變量x。調用函數f時,參數形成一個單獨的作用域。在這個作用域裏面,默認值變量x指向第一個參數x,而不是全局變量x,所以輸出是Crazy Crazy。
let x = 'Blue';
let fun = (y = x) => {
let x = 'Crazy';
console.log(y);
}
fun(); //Blue
上面代碼中,函數調用時,參數y = x形成一個單獨的作用域。這個作用域裏面,變量x本身沒有定義,所以指向外層的全局變量x。函數調用時,由於塊級作用域,所以函數體內部的局部變量x影響不到默認值變量x。
上面例子中因爲默認值賦值的X變量是指向外層的全局x,所以如果全局沒有x則會報錯
let fun = (y = x) => {
let x = 'Crazy';
console.log(y);
}
fun(); // 'ReferenceError: x is not defined'
下面這樣子寫也會報錯
var x = 1;
function foo(x = x) {
// ...
}
foo() // ReferenceError: x is not defined
上面代碼中,參數x = x形成一個單獨作用域。實際執行的是let x = x,由於暫時性死區的原因,這行代碼會報錯”x 未定義“。
1.6 默認值應用
利用參數默認值,可以指定某一個參數不得省略,如果省略就拋出一個錯誤。
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
上面代碼中參數默認值設置爲一個函數,由於是【惰性求值】,所以foo在運行時,如果沒有傳入參數則執行函數拋出錯誤。
另外,可以將參數默認值設爲undefined,表明這個參數是可以省略的
function foo(optional = undefined) { ··· }
2.rest參數
什麼事rest參數,形式爲(…變量名)這樣子的函數參數我們稱之爲rest參數,rest參數將多餘的組合成一個數組。
function fun(...values) {
console.log(values);
}
fun(1, 2, 3, 4)
//[ 1, 2, 3, 4 ]
上面代碼中rest參數將所有傳入的參數都放入了values數組中,這樣就完全取代了arguments對象,並且擁有更多的屬性。
【注意】rest 參數之後不能再有其他參數(即只能是最後一個參數),否則會報錯。
// 報錯
function f(a, ...b, c) {
// ...
}
函數的length屬性,不包括 rest 參數。
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
3.嚴格模式
ES6規定,只要函數參數使用了默認值、解構賦值、或者擴展運算符,那麼函數內部就不能顯式設定爲嚴格模式,否則會報錯。
// 報錯
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
}
};
4.name屬性
函數的name屬性,返回該函數的函數名。
function foo() {}
foo.name // "foo"
這個屬性是在ES5中就有的,但是ES6對其做了一些修改,如果將一個匿名函數賦值給一個變量,ES5 的name屬性,會返回空字符串,而 ES6 的name屬性會返回實際的函數名。
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
上面代碼中,變量f等於一個匿名函數,ES5 和 ES6 的name屬性返回的值不一樣。
如果將一個具名函數賦值給一個變量,則 ES5 和 ES6 的name屬性都返回這個具名函數原本的名字。
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
Function構造函數返回的函數實例,name屬性的值爲anonymous。
(new Function).name // "anonymous"
bind返回的函數,name屬性值會加上bound前綴。
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
5.箭頭函數
5.1 基本用法
以前的函數定義就是function,ES6使得這一切更加簡單清晰
let f = x => x;
上面代碼翻譯跟ES5就是下面這樣子
var f = function f(x) {
return x;
};
和if語句一樣,代碼庫多餘一條語句,就要使用大括號將它們括起來,並且要返回值時使用return語句
let f = x => {
if (typeof x === undefined) {
x = 'Blue';
}
return x;
};
由於大括號被解釋爲代碼塊,所以如果箭頭函數直接返回一個對象,必須在對象外面加上括號。
var getTempItem = id => ({ id: id, name: "Temp" });
既然箭頭函數也是函數,那麼參數同樣可以與變量解構結合使用
let fun = ({ x, y }) => console.log(x + y);
fun({ x: 1, y: 2 }); //3
上面代碼爲對象的解構賦值和箭頭函數的應用
let fun = ([x, y]) => console.log(x + y);
fun([1, 2]); //3
上面代碼爲數組的解構賦值和箭頭函數的應用
如果參數列表只有一個參數,則可以省略參數列表的括號
[1, 2, 3, 4].map(x => x * 10);
//[ 10, 20, 30, 40 ]
如果箭頭函數不需要參數或者需要多個參數,則必須使用圓括號代表安琥是列表
let fun = () => 'Blue';
let fun = (x, y, z) => x + y + z;
既然正常函數可以與rest參數聯合使用,那麼箭頭函數也可以
const mkArr = (...arr) => arr;
mkArr(1, 2, 3, 4); //[1, 2, 3, 4]
const mkArr = (first,...arr) => [first,arr];
mkArr(1, 2, 3, 4);
//[1, [2, 3, 4]]
5.2 使用箭頭函數的注意點
- 函數體內的this對象,就是【定義時所在的對象】,而不是使用時所在的對象。
- 【不可以當作構造函數】,也就是說,不可以使用new命令,否則會拋出一個錯誤。
- 【不可以使用arguments對象】,該對象在函數體內不存在。如果要用,可以用 rest 參數代替。
- 【不可以使用yield命令】,因此箭頭函數不能用作 Generator 函數。
function fun() {
setTimeout(() => {
console.log('name:', this.name);
}, 100);
}
var name = 'Crazy';
fun.call({ name: 'Blue' }); //name: Blue
上面代碼中,setTimeout的參數是一個箭頭函數,這個箭頭函數的定義生效是在fun函數生成時,而它的真正執行要等到100毫秒後。如果是普通函數,執行時this應該指向全局對象window,這時應該輸出Crazy。但是,箭頭函數導致this總是指向函數定義生效時所在的對象(本例是{ name: ‘Blue’ }),所以輸出的是Blue。
箭頭函數可以讓setTimeout裏面的this,綁定定義時所在的作用域,而不是指向運行時所在的作用域。下面是另一個例子。
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭頭函數
setInterval(() => this.s1++, 1000);
// 普通函數
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
上面代碼中,Timer函數內部設置了兩個定時器,分別使用了箭頭函數和普通函數。前者的this綁定定義時所在的作用域(即Timer函數),後者的this指向運行時所在的作用域(即全局對象)。所以,3100毫秒之後,timer.s1被更新了3次,而timer.s2一次都沒更新。