ES6 之前,不能直接為函數(shù)的參數(shù)指定默認(rèn)值,只能采用變通的方法。
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World
上面代碼檢查函數(shù) log 的參數(shù) y 有沒有賦值,如果沒有,則指定默認(rèn)值為 World 。這種寫法的缺點(diǎn)在于,如果參數(shù) y 賦值了,但是對(duì)應(yīng)的布爾值為 false ,則該賦值不起作用。就像上面代碼的最后一行,參數(shù) y 等于空字符,結(jié)果被改為默認(rèn)值。
為了避免這個(gè)問題,通常需要先判斷一下參數(shù) y 是否被賦值,如果沒有,再等于默認(rèn)值。
if (typeof y === 'undefined') {
y = 'World';
}
ES6 允許為函數(shù)的參數(shù)設(shè)置默認(rèn)值,即直接寫在參數(shù)定義的后面。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
可以看到,ES6 的寫法比 ES5 簡(jiǎn)潔許多,而且非常自然。下面是另一個(gè)例子。
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
const p = new Point();
p // { x: 0, y: 0 }
除了簡(jiǎn)潔
,ES6 的寫法還有兩個(gè)好處:首先,閱讀代碼的人,可以立刻意識(shí)到哪些參數(shù)是可以省略的,不用查看函數(shù)體或文檔;其次,有利于將來的代碼優(yōu)化
,即使未來的版本在對(duì)外接口中,徹底拿掉這個(gè)參數(shù),也不會(huì)導(dǎo)致以前的代碼無法運(yùn)行。
參數(shù)變量是默認(rèn)聲明的,所以不能用 let 或 const 再次聲明。
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
上面代碼中,參數(shù)變量 x 是默認(rèn)聲明的,在函數(shù)體中,不能用 let 或 const 再次聲明,否則會(huì)報(bào)錯(cuò)。
使用參數(shù)默認(rèn)值時(shí),函數(shù)不能有同名參數(shù)。
// 不報(bào)錯(cuò)
function foo(x, x, y) {
// ...
}
// 報(bào)錯(cuò)
function foo(x, x, y = 1) {
// ...
}
// SyntaxError: Duplicate parameter name not allowed in this context
另外,一個(gè)容易忽略的地方是,參數(shù)默認(rèn)值不是傳值的,而是每次都重新計(jì)算默認(rèn)值表達(dá)式的值。也就是說,參數(shù)默認(rèn)值是惰性求值的。
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
上面代碼中,參數(shù) p 的默認(rèn)值是 x + 1 。這時(shí),每次調(diào)用函數(shù) foo ,都會(huì)重新計(jì)算 x + 1 ,而不是默認(rèn) p 等于 100。
參數(shù)默認(rèn)值
可以與解構(gòu)賦值
的默認(rèn)值,結(jié)合起來使用。
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
上面代碼只使用了對(duì)象的解構(gòu)賦值默認(rèn)值,沒有使用函數(shù)參數(shù)的默認(rèn)值。只有當(dāng)函數(shù) foo 的參數(shù)是一個(gè)對(duì)象時(shí),變量 x 和 y 才會(huì)通過解構(gòu)賦值生成。如果函數(shù) foo 調(diào)用時(shí)沒提供參數(shù),變量 x 和 y 就不會(huì)生成,從而報(bào)錯(cuò)。通過提供函數(shù)參數(shù)的默認(rèn)值,就可以避免這種情況。
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5
上面代碼指定,如果沒有提供參數(shù),函數(shù) foo 的參數(shù)默認(rèn)為一個(gè)空對(duì)象。
下面是另一個(gè)解構(gòu)賦值默認(rèn)值的例子。
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {})
// "GET"
fetch('http://example.com')
// 報(bào)錯(cuò)
上面代碼中,如果函數(shù) fetch 的第二個(gè)參數(shù)是一個(gè)對(duì)象,就可以為它的三個(gè)屬性設(shè)置默認(rèn)值。這種寫法不能省略第二個(gè)參數(shù),如果結(jié)合函數(shù)參數(shù)的默認(rèn)值,就可以省略第二個(gè)參數(shù)。這時(shí),就出現(xiàn)了雙重默認(rèn)值。
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
上面代碼中,函數(shù) fetch 沒有第二個(gè)參數(shù)時(shí),函數(shù)參數(shù)的默認(rèn)值就會(huì)生效,然后才是解構(gòu)賦值的默認(rèn)值生效,變量 method 才會(huì)取到默認(rèn)值 GET 。
作為練習(xí),請(qǐng)問下面兩種寫法有什么差別?
// 寫法一
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 寫法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
上面兩種寫法都對(duì)函數(shù)的參數(shù)設(shè)定了默認(rèn)值,區(qū)別是寫法一函數(shù)參數(shù)的默認(rèn)值是空對(duì)象,但是設(shè)置了對(duì)象解構(gòu)賦值的默認(rèn)值;寫法二函數(shù)參數(shù)的默認(rèn)值是一個(gè)有具體屬性的對(duì)象,但是沒有設(shè)置對(duì)象解構(gòu)賦值的默認(rèn)值。
// 函數(shù)沒有參數(shù)的情況
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]
通常情況下,定義了默認(rèn)值的參數(shù),應(yīng)該是函數(shù)的尾參數(shù)
。因?yàn)檫@樣比較容易看出來,到底省略了哪些參數(shù)。如果非尾部的參數(shù)設(shè)置默認(rèn)值,實(shí)際上這個(gè)參數(shù)是沒法省略的。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 報(bào)錯(cuò)
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) // 報(bào)錯(cuò)
f(1, undefined, 2) // [1, 5, 2]
上面代碼中,有默認(rèn)值的參數(shù)都不是尾參數(shù)。這時(shí),無法只省略該參數(shù),而不省略它后面的參數(shù),除非顯式輸入 undefined 。
如果傳入 undefined ,將觸發(fā)該參數(shù)等于默認(rèn)值, null 則沒有這個(gè)效果。
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null)
// 5 null
上面代碼中, x 參數(shù)對(duì)應(yīng) undefined ,結(jié)果觸發(fā)了默認(rèn)值, y 參數(shù)等于 null ,就沒有觸發(fā)默認(rèn)值。
指定了默認(rèn)值以后,函數(shù)的 length 屬性,將返回沒有指定默認(rèn)值的參數(shù)個(gè)數(shù)。也就是說,指定了默認(rèn)值
后, length 屬性將失真
。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
上面代碼中, length 屬性的返回值,等于函數(shù)的參數(shù)個(gè)數(shù)減去指定了默認(rèn)值的參數(shù)個(gè)數(shù)。比如,上面最后一個(gè)函數(shù),定義了 3 個(gè)參數(shù),其中有一個(gè)參數(shù) c 指定了默認(rèn)值,因此 length 屬性等于 3 減去 1 ,最后得到 2 。
這是因?yàn)? length 屬性的含義是,該函數(shù)預(yù)期傳入的參數(shù)個(gè)數(shù)。某個(gè)參數(shù)指定默認(rèn)值以后,預(yù)期傳入的參數(shù)個(gè)數(shù)就不包括這個(gè)參數(shù)了。同理,后文的 rest 參數(shù)也不會(huì)計(jì)入 length 屬性。
(function(...args) {}).length // 0
如果設(shè)置了默認(rèn)值的參數(shù)不是尾參數(shù),那么 length 屬性也不再計(jì)入后面的參數(shù)了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
一旦設(shè)置了參數(shù)的默認(rèn)值,函數(shù)進(jìn)行聲明初始化時(shí),參數(shù)會(huì)形成一個(gè)單獨(dú)的作用域(context)
。等到初始化結(jié)束,這個(gè)作用域就會(huì)消失。這種語法行為,在不設(shè)置參數(shù)默認(rèn)值時(shí),是不會(huì)出現(xiàn)的。
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
上面代碼中,參數(shù) y 的默認(rèn)值等于變量 x 。調(diào)用函數(shù) f 時(shí),參數(shù)形成一個(gè)單獨(dú)的作用域。在這個(gè)作用域里面,默認(rèn)值變量 x 指向第一個(gè)參數(shù) x ,而不是全局變量 x ,所以輸出是 2 。
再看下面的例子。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
上面代碼中,函數(shù) f 調(diào)用時(shí),參數(shù) y = x 形成一個(gè)單獨(dú)的作用域。這個(gè)作用域里面,變量 x 本身沒有定義,所以指向外層的全局變量 x 。函數(shù)調(diào)用時(shí),函數(shù)體內(nèi)部的局部變量 x 影響不到默認(rèn)值變量 x 。
如果此時(shí),全局變量 x 不存在,就會(huì)報(bào)錯(cuò)。
function f(y = x) {
let x = 2;
console.log(y);
}
f() // ReferenceError: x is not defined
下面這樣寫,也會(huì)報(bào)錯(cuò)。
var x = 1;
function foo(x = x) {
// ...
}
foo() // ReferenceError: x is not defined
上面代碼中,參數(shù) x = x 形成一個(gè)單獨(dú)作用域。實(shí)際執(zhí)行的是 let x = x ,由于暫時(shí)性死區(qū)的原因,這行代碼會(huì)報(bào)錯(cuò)”x 未定義“。
如果參數(shù)的默認(rèn)值是一個(gè)函數(shù),該函數(shù)的作用域也遵守這個(gè)規(guī)則。請(qǐng)看下面的例子。
let foo = 'outer';
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar(); // outer
上面代碼中,函數(shù) bar 的參數(shù) func 的默認(rèn)值是一個(gè)匿名函數(shù),返回值為變量 foo 。函數(shù)參數(shù)形成的單獨(dú)作用域里面,并沒有定義變量 foo ,所以 foo 指向外層的全局變量 foo ,因此輸出 outer 。
如果寫成下面這樣,就會(huì)報(bào)錯(cuò)。
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar() // ReferenceError: foo is not defined
上面代碼中,匿名函數(shù)里面的 foo 指向函數(shù)外層,但是函數(shù)外層并沒有聲明變量 foo ,所以就報(bào)錯(cuò)了。
下面是一個(gè)更復(fù)雜的例子。
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
foo() // 3
x // 1
上面代碼中,函數(shù) foo 的參數(shù)形成一個(gè)單獨(dú)作用域。這個(gè)作用域里面,首先聲明了變量 x ,然后聲明了變量 y , y 的默認(rèn)值是一個(gè)匿名函數(shù)。這個(gè)匿名函數(shù)內(nèi)部的變量 x ,指向同一個(gè)作用域的第一個(gè)參數(shù) x 。函數(shù) foo 內(nèi)部又聲明了一個(gè)內(nèi)部變量 x ,該變量與第一個(gè)參數(shù) x 由于不是同一個(gè)作用域,所以不是同一個(gè)變量,因此執(zhí)行 y 后,內(nèi)部變量 x 和外部全局變量 x 的值都沒變。
如果將 var x = 3 的 var 去除,函數(shù) foo 的內(nèi)部變量 x 就指向第一個(gè)參數(shù) x ,與匿名函數(shù)內(nèi)部的 x 是一致的,所以最后輸出的就是 2 ,而外層的全局變量 x 依然不受影響。
var x = 1;
function foo(x, y = function() { x = 2; }) {
x = 3;
y();
console.log(x);
}
foo() // 2
x // 1
利用參數(shù)默認(rèn)值,可以指定某一個(gè)參數(shù)不得省略,如果省略就拋出一個(gè)錯(cuò)誤。
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
上面代碼的 foo 函數(shù),如果調(diào)用的時(shí)候沒有參數(shù),就會(huì)調(diào)用默認(rèn)值 throwIfMissing 函數(shù),從而拋出一個(gè)錯(cuò)誤。
從上面代碼還可以看到,參數(shù) mustBeProvided 的默認(rèn)值等于 throwIfMissing 函數(shù)的運(yùn)行結(jié)果(注意函數(shù)名 throwIfMissing 之后有一對(duì)圓括號(hào)),這表明參數(shù)的默認(rèn)值不是在定義時(shí)執(zhí)行,而是在運(yùn)行時(shí)執(zhí)行。如果參數(shù)已經(jīng)賦值,默認(rèn)值中的函數(shù)就不會(huì)運(yùn)行。
另外,可以將參數(shù)默認(rèn)值設(shè)為 undefined ,表明這個(gè)參數(shù)是可以省略的。
function foo(optional = undefined) { ··· }
ES6 引入rest 參數(shù)
(形式為 ...變量名 ),用于獲取函數(shù)的多余參數(shù),這樣就不需要使用 arguments 對(duì)象了。rest 參數(shù)搭配的變量是一個(gè)數(shù)組
,該變量將多余的參數(shù)放入數(shù)組中。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
上面代碼的add
函數(shù)是一個(gè)求和函數(shù)
,利用 rest 參數(shù),可以向該函數(shù)傳入任意數(shù)目的參數(shù)。
下面是一個(gè) rest 參數(shù)代替 arguments 變量的例子。
// arguments變量的寫法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest參數(shù)的寫法
const sortNumbers = (...numbers) => numbers.sort();
上面代碼的兩種寫法,比較后可以發(fā)現(xiàn),rest 參數(shù)的寫法更自然也更簡(jiǎn)潔。
arguments 對(duì)象不是數(shù)組,而是一個(gè)類似數(shù)組的對(duì)象。所以為了使用數(shù)組的方法,必須使用 Array.prototype.slice.call 先將其轉(zhuǎn)為數(shù)組。rest 參數(shù)就不存在這個(gè)問題,它就是一個(gè)真正的數(shù)組,數(shù)組特有的方法都可以使用。下面是一個(gè)利用 rest 參數(shù)改寫數(shù)組 push 方法的例子。
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
注意,rest 參數(shù)之后不能再有其他參數(shù)(即只能是最后一個(gè)參數(shù)),否則會(huì)報(bào)錯(cuò)。
// 報(bào)錯(cuò)
function f(a, ...b, c) {
// ...
}
函數(shù)的 length 屬性,不包括 rest 參數(shù)。
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
從ES5
開始,函數(shù)內(nèi)部可以設(shè)定為嚴(yán)格模式
。
function doSomething(a, b) {
'use strict';
// code
}
ES2016
做了一點(diǎn)修改,規(guī)定只要函數(shù)參數(shù)使用了默認(rèn)值
、解構(gòu)賦值
、或者擴(kuò)展運(yùn)算符
,那么函數(shù)內(nèi)部就不能顯式設(shè)定為嚴(yán)格模式,否則會(huì)報(bào)錯(cuò)。
// 報(bào)錯(cuò)
function doSomething(a, b = a) {
'use strict';
// code
}
// 報(bào)錯(cuò)
const doSomething = function ({a, b}) {
'use strict';
// code
};
// 報(bào)錯(cuò)
const doSomething = (...a) => {
'use strict';
// code
};
const obj = {
// 報(bào)錯(cuò)
doSomething({a, b}) {
'use strict';
// code
}
};
這樣規(guī)定的原因是,函數(shù)內(nèi)部的嚴(yán)格模式,同時(shí)適用于函數(shù)體
和函數(shù)參數(shù)
。但是,函數(shù)執(zhí)行的時(shí)候,先執(zhí)行函數(shù)參數(shù),然后再執(zhí)行函數(shù)體。這樣就有一個(gè)不合理的地方,只有從函數(shù)體之中,才能知道參數(shù)是否應(yīng)該以嚴(yán)格模式執(zhí)行,但是參數(shù)卻應(yīng)該先于函數(shù)體執(zhí)行。
// 報(bào)錯(cuò)
function doSomething(value = 070) {
'use strict';
return value;
}
上面代碼中,參數(shù) value 的默認(rèn)值是八進(jìn)制數(shù) 070 ,但是嚴(yán)格模式下不能用前綴 0 表示八進(jìn)制,所以應(yīng)該報(bào)錯(cuò)。但是實(shí)際上,JavaScript 引擎會(huì)先成功執(zhí)行 value = 070 ,然后進(jìn)入函數(shù)體內(nèi)部,發(fā)現(xiàn)需要用嚴(yán)格模式執(zhí)行,這時(shí)才會(huì)報(bào)錯(cuò)。
雖然可以先解析函數(shù)體代碼,再執(zhí)行參數(shù)代碼,但是這樣無疑就增加了復(fù)雜性。因此,標(biāo)準(zhǔn)索性禁止了這種用法,只要參數(shù)使用了默認(rèn)值、解構(gòu)賦值、或者擴(kuò)展運(yùn)算符,就不能顯式指定嚴(yán)格模式。
兩種方法可以規(guī)避這種限制。第一種是設(shè)定全局性的嚴(yán)格模式,這是合法的。
'use strict';
function doSomething(a, b = a) {
// code
}
第二種是把函數(shù)包在一個(gè)無參數(shù)的立即執(zhí)行函數(shù)里面。
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
函數(shù)的name
屬性,返回該函數(shù)的函數(shù)名
。
function foo() {}
foo.name // "foo"
這個(gè)屬性早就被瀏覽器廣泛支持,但是直到 ES6,才將其寫入了標(biāo)準(zhǔn)。
需要注意的是,ES6 對(duì)這個(gè)屬性的行為做出了一些修改。如果將一個(gè)匿名函數(shù)賦值給一個(gè)變量,ES5 的 name 屬性,會(huì)返回空字符串,而 ES6 的 name 屬性會(huì)返回實(shí)際的函數(shù)名。
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
上面代碼中,變量 f 等于一個(gè)匿名函數(shù),ES5 和 ES6 的 name 屬性返回的值不一樣。
如果將一個(gè)具名函數(shù)賦值給一個(gè)變量,則 ES5 和 ES6 的 name 屬性都返回這個(gè)具名函數(shù)原本的名字。
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
Function 構(gòu)造函數(shù)返回的函數(shù)實(shí)例, name 屬性的值為 anonymous 。
(new Function).name // "anonymous"
bind 返回的函數(shù), name 屬性值會(huì)加上 bound 前綴。
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
ES6
允許使用“箭頭”
( => )定義函數(shù)。
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
如果箭頭函數(shù)不需要參數(shù)或需要多個(gè)參數(shù),就使用一個(gè)圓括號(hào)
代表參數(shù)部分。
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
如果箭頭函數(shù)的代碼塊部分多于一條語句,就要使用大括號(hào)將它們括起來,并且使用 return 語句返回。
var sum = (num1, num2) => { return num1 + num2; }
由于大括號(hào)被解釋為代碼塊,所以如果箭頭函數(shù)直接返回一個(gè)對(duì)象,必須在對(duì)象外面加上括號(hào),否則會(huì)報(bào)錯(cuò)。
// 報(bào)錯(cuò)
let getTempItem = id => { id: id, name: "Temp" };
// 不報(bào)錯(cuò)
let getTempItem = id => ({ id: id, name: "Temp" });
下面是一種特殊情況,雖然可以運(yùn)行,但會(huì)得到錯(cuò)誤的結(jié)果。
let foo = () => { a: 1 };
foo() // undefined
上面代碼中,原始意圖是返回一個(gè)對(duì)象 { a: 1 } ,但是由于引擎認(rèn)為大括號(hào)是代碼塊,所以執(zhí)行了一行語句 a: 1 。這時(shí), a 可以被解釋為語句的標(biāo)簽,因此實(shí)際執(zhí)行的語句是 1; ,然后函數(shù)就結(jié)束了,沒有返回值。
如果箭頭函數(shù)只有一行語句,且不需要返回值,可以采用下面的寫法,就不用寫大括號(hào)了。
let fn = () => void doesNotReturn();
箭頭函數(shù)
可以與變量解構(gòu)
結(jié)合使用。
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
箭頭函數(shù)使得表達(dá)更加簡(jiǎn)潔
。
const isEven = n => n % 2 === 0;
const square = n => n * n;
上面代碼只用了兩行,就定義了兩個(gè)簡(jiǎn)單的工具函數(shù)。如果不用箭頭函數(shù),可能就要占用多行,而且還不如現(xiàn)在這樣寫醒目。
箭頭函數(shù)的一個(gè)用處是簡(jiǎn)化回調(diào)函數(shù)。
// 正常函數(shù)寫法
[1,2,3].map(function (x) {
return x * x;
});
// 箭頭函數(shù)寫法
[1,2,3].map(x => x * x);
另一個(gè)例子是
// 正常函數(shù)寫法
var result = values.sort(function (a, b) {
return a - b;
});
// 箭頭函數(shù)寫法
var result = values.sort((a, b) => a - b);
下面是 rest 參數(shù)與箭頭函數(shù)結(jié)合的例子。
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]]
箭頭函數(shù)有幾個(gè)使用注意點(diǎn)。
(1)函數(shù)體內(nèi)的 this
對(duì)象,就是定義時(shí)所在的對(duì)象,而不是使用時(shí)所在的對(duì)象。
(2)不可以當(dāng)作構(gòu)造函數(shù)
,也就是說,不可以使用 new 命令,否則會(huì)拋出一個(gè)錯(cuò)誤。
(3)不可以使用arguments
對(duì)象,該對(duì)象在函數(shù)體內(nèi)不存在。如果要用,可以用 rest 參數(shù)代替。
(4)不可以使用 yield
命令,因此箭頭函數(shù)不能用作 Generator 函數(shù)。
上面四點(diǎn)中,第一點(diǎn)尤其值得注意。 this 對(duì)象的指向是可變的,但是在箭頭函數(shù)中,它是固定的。
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
上面代碼中,setTimeout
的參數(shù)是一個(gè)箭頭函數(shù),這個(gè)箭頭函數(shù)的定義生效是在foo
函數(shù)生成時(shí),而它的真正執(zhí)行要等到100 毫秒
后。如果是普通函數(shù),執(zhí)行時(shí) this 應(yīng)該指向全局對(duì)象 window ,這時(shí)應(yīng)該輸出 21 。但是,箭頭函數(shù)導(dǎo)致 this 總是指向函數(shù)定義生效時(shí)所在的對(duì)象(本例是 {id: 42} ),所以輸出的是 42 。
箭頭函數(shù)可以讓 setTimeout 里面的 this ,綁定定義時(shí)所在的作用域,而不是指向運(yùn)行時(shí)所在的作用域。下面是另一個(gè)例子。
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭頭函數(shù)
setInterval(() => this.s1++, 1000);
// 普通函數(shù)
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 函數(shù)內(nèi)部設(shè)置了兩個(gè)定時(shí)器,分別使用了箭頭函數(shù)和普通函數(shù)。前者的 this 綁定定義時(shí)所在的作用域(即 Timer 函數(shù)),后者的 this 指向運(yùn)行時(shí)所在的作用域(即全局對(duì)象)。所以,3100 毫秒之后, timer.s1 被更新了 3 次,而 timer.s2 一次都沒更新。
箭頭函數(shù)可以讓 this 指向固定化,這種特性很有利于封裝回調(diào)函數(shù)。下面是一個(gè)例子,DOM 事件的回調(diào)函數(shù)封裝在一個(gè)對(duì)象里面。
var handler = {
id: '123456',
init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id);
}
};
上面代碼的 init 方法中,使用了箭頭函數(shù),這導(dǎo)致這個(gè)箭頭函數(shù)里面的 this ,總是指向 handler 對(duì)象。否則,回調(diào)函數(shù)運(yùn)行時(shí), this.doSomething 這一行會(huì)報(bào)錯(cuò),因?yàn)榇藭r(shí) this 指向 document 對(duì)象。
this 指向的固定化,并不是因?yàn)榧^函數(shù)內(nèi)部有綁定 this 的機(jī)制,實(shí)際原因是箭頭函數(shù)根本沒有自己的 this ,導(dǎo)致內(nèi)部的 this 就是外層代碼塊的 this 。正是因?yàn)樗鼪]有 this ,所以也就不能用作構(gòu)造函數(shù)。
所以,箭頭函數(shù)轉(zhuǎn)成 ES5 的代碼如下。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
上面代碼中,轉(zhuǎn)換后的 ES5 版本清楚地說明了,箭頭函數(shù)里面根本沒有自己的 this ,而是引用外層的 this 。
請(qǐng)問下面的代碼之中有幾個(gè) this ?
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
上面代碼之中,只有一個(gè) this ,就是函數(shù) foo 的 this ,所以 t1 、 t2 、 t3 都輸出同樣的結(jié)果。因?yàn)樗械膬?nèi)層函數(shù)都是箭頭函數(shù),都沒有自己的 this ,它們的 this 其實(shí)都是最外層 foo 函數(shù)的 this 。
除了 this ,以下三個(gè)變量在箭頭函數(shù)之中也是不存在的,指向外層函數(shù)的對(duì)應(yīng)變量: arguments 、 super 、 new.target 。
function foo() {
setTimeout(() => {
console.log('args:', arguments);
}, 100);
}
foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
上面代碼中,箭頭函數(shù)內(nèi)部的變量 arguments ,其實(shí)是函數(shù) foo 的 arguments 變量。
另外,由于箭頭函數(shù)沒有自己的 this ,所以當(dāng)然也就不能用 call() 、 apply() 、 bind() 這些方法去改變 this 的指向。
(function() {
return [
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });
// ['outer']
上面代碼中,箭頭函數(shù)沒有自己的 this ,所以 bind 方法無效,內(nèi)部的 this 指向外部的 this 。
長(zhǎng)期以來,JavaScript 語言的 this 對(duì)象一直是一個(gè)令人頭痛的問題,在對(duì)象方法中使用 this ,必須非常小心。箭頭函數(shù)”綁定” this ,很大程度上解決了這個(gè)困擾。
由于箭頭函數(shù)使得 this 從“動(dòng)態(tài)”
變成“靜態(tài)”
,下面兩個(gè)場(chǎng)合不應(yīng)該使用箭頭函數(shù)。
第一個(gè)場(chǎng)合是定義對(duì)象
的方法,且該方法內(nèi)部包括this
。
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
上面代碼中, cat.jumps() 方法是一個(gè)箭頭函數(shù),這是錯(cuò)誤的。調(diào)用 cat.jumps() 時(shí),如果是普通函數(shù),該方法內(nèi)部的 this 指向 cat ;如果寫成上面那樣的箭頭函數(shù),使得 this 指向全局對(duì)象,因此不會(huì)得到預(yù)期結(jié)果。這是因?yàn)閷?duì)象不構(gòu)成單獨(dú)的作用域,導(dǎo)致 jumps 箭頭函數(shù)定義時(shí)的作用域就是全局作用域。
第二個(gè)場(chǎng)合是需要?jiǎng)討B(tài) this 的時(shí)候,也不應(yīng)使用箭頭函數(shù)。
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
上面代碼運(yùn)行時(shí),點(diǎn)擊按鈕會(huì)報(bào)錯(cuò),因?yàn)? button 的監(jiān)聽函數(shù)是一個(gè)箭頭函數(shù),導(dǎo)致里面的 this 就是全局對(duì)象。如果改成普通函數(shù), this 就會(huì)動(dòng)態(tài)指向被點(diǎn)擊的按鈕對(duì)象。
另外,如果函數(shù)體很復(fù)雜,有許多行,或者函數(shù)內(nèi)部有大量的讀寫操作,不單純是為了計(jì)算值,這時(shí)也不應(yīng)該使用箭頭函數(shù),而是要使用普通函數(shù),這樣可以提高代碼可讀性。
箭頭函數(shù)內(nèi)部,還可以再使用箭頭函數(shù)。下面是一個(gè) ES5 語法的多重嵌套
函數(shù)。
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]
上面這個(gè)函數(shù),可以使用箭頭函數(shù)改寫。
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]
下面是一個(gè)部署管道機(jī)制(pipeline)的例子,即前一個(gè)函數(shù)的輸出是后一個(gè)函數(shù)的輸入。
const pipeline = (...funcs) =>
val => funcs.reduce((a, b) => b(a), val);
const plus1 = a => a + 1;
const mult2 = a => a * 2;
const addThenMult = pipeline(plus1, mult2);
addThenMult(5)
// 12
如果覺得上面的寫法可讀性比較差,也可以采用下面的寫法。
const plus1 = a => a + 1;
const mult2 = a => a * 2;
mult2(plus1(5))
// 12
箭頭函數(shù)還有一個(gè)功能,就是可以很方便地改寫 λ 演算。
// λ演算的寫法
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
// ES6的寫法
var fix = f => (x => f(v => x(x)(v)))
(x => f(v => x(x)(v)));
上面兩種寫法,幾乎是一一對(duì)應(yīng)的。由于 λ 演算對(duì)于計(jì)算機(jī)科學(xué)非常重要,這使得我們可以用 ES6 作為替代工具,探索計(jì)算機(jī)科學(xué)。
尾調(diào)用
(Tail Call)是函數(shù)式編程的一個(gè)重要概念,本身非常簡(jiǎn)單,一句話就能說清楚,就是指某個(gè)函數(shù)的最后一步是調(diào)用另一個(gè)函數(shù)。
function f(x){
return g(x);
}
上面代碼中,函數(shù) f 的最后一步是調(diào)用函數(shù) g ,這就叫尾調(diào)用。
以下三種情況,都不屬于尾調(diào)用。
// 情況一
function f(x){
let y = g(x);
return y;
}
// 情況二
function f(x){
return g(x) + 1;
}
// 情況三
function f(x){
g(x);
}
上面代碼中,情況一是調(diào)用函數(shù) g 之后,還有賦值操作,所以不屬于尾調(diào)用,即使語義完全一樣。情況二也屬于調(diào)用后還有操作,即使寫在一行內(nèi)。情況三等同于下面的代碼。
function f(x){
g(x);
return undefined;
}
尾調(diào)用不一定出現(xiàn)在函數(shù)尾部,只要是最后一步操作即可。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
上面代碼中,函數(shù) m 和 n 都屬于尾調(diào)用,因?yàn)樗鼈兌际呛瘮?shù) f 的最后一步操作。
尾調(diào)用之所以與其他調(diào)用不同,就在于它的特殊的調(diào)用位置
。
我們知道,函數(shù)調(diào)用會(huì)在內(nèi)存形成一個(gè)“調(diào)用記錄”
,又稱“調(diào)用幀”
(call frame),保存調(diào)用位置和內(nèi)部變量等信息。如果在函數(shù) A 的內(nèi)部調(diào)用函數(shù) B ,那么在 A 的調(diào)用幀上方,還會(huì)形成一個(gè) B 的調(diào)用幀。等到 B 運(yùn)行結(jié)束,將結(jié)果返回到 A , B 的調(diào)用幀才會(huì)消失。如果函數(shù) B 內(nèi)部還調(diào)用函數(shù) C ,那就還有一個(gè) C 的調(diào)用幀,以此類推。所有的調(diào)用幀,就形成一個(gè)“調(diào)用?!保╟all stack)。
尾調(diào)用由于是函數(shù)的最后一步操作,所以不需要保留外層函數(shù)的調(diào)用幀,因?yàn)檎{(diào)用位置、內(nèi)部變量等信息都不會(huì)再用到了,只要直接用內(nèi)層函數(shù)的調(diào)用幀,取代外層函數(shù)的調(diào)用幀就可以了。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
上面代碼中,如果函數(shù) g 不是尾調(diào)用,函數(shù) f 就需要保存內(nèi)部變量 m 和 n 的值、 g 的調(diào)用位置等信息。但由于調(diào)用 g 之后,函數(shù) f 就結(jié)束了,所以執(zhí)行到最后一步,完全可以刪除 f(x) 的調(diào)用幀,只保留 g(3) 的調(diào)用幀。
這就叫做“尾調(diào)用優(yōu)化”(Tail call optimization),即只保留內(nèi)層函數(shù)的調(diào)用幀。如果所有函數(shù)都是尾調(diào)用,那么完全可以做到每次執(zhí)行時(shí),調(diào)用幀只有一項(xiàng),這將大大節(jié)省內(nèi)存。這就是“尾調(diào)用優(yōu)化”的意義。
注意,只有不再用到外層函數(shù)的內(nèi)部變量,內(nèi)層函數(shù)的調(diào)用幀才會(huì)取代外層函數(shù)的調(diào)用幀,否則就無法進(jìn)行“尾調(diào)用優(yōu)化”。
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}
上面的函數(shù)不會(huì)進(jìn)行尾調(diào)用優(yōu)化,因?yàn)閮?nèi)層函數(shù) inner 用到了外層函數(shù) addOne 的內(nèi)部變量 one 。
注意,目前只有 Safari 瀏覽器支持尾調(diào)用優(yōu)化,Chrome 和 Firefox 都不支持。
函數(shù)調(diào)用自身,稱為遞歸
。如果尾調(diào)用自身,就稱為尾遞歸
。
遞歸非常耗費(fèi)內(nèi)存
,因?yàn)樾枰瑫r(shí)保存成千上百個(gè)調(diào)用幀,很容易發(fā)生“棧溢出”
錯(cuò)誤(stack overflow)。但對(duì)于尾遞歸來說,由于只存在一個(gè)調(diào)用幀,所以永遠(yuǎn)不會(huì)發(fā)生“棧溢出”錯(cuò)誤。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
上面代碼是一個(gè)階乘函數(shù),計(jì)算 n 的階乘,最多需要保存 n 個(gè)調(diào)用記錄,復(fù)雜度 O(n) 。
如果改寫成尾遞歸,只保留一個(gè)調(diào)用記錄,復(fù)雜度 O(1) 。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
還有一個(gè)比較著名的例子,就是計(jì)算 Fibonacci 數(shù)列,也能充分說明尾遞歸優(yōu)化的重要性。
非尾遞歸的 Fibonacci 數(shù)列實(shí)現(xiàn)如下。
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 超時(shí)
Fibonacci(500) // 超時(shí)
尾遞歸優(yōu)化過的 Fibonacci 數(shù)列實(shí)現(xiàn)如下。
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
由此可見,“尾調(diào)用優(yōu)化”
對(duì)遞歸操作意義重大,所以一些函數(shù)式編程語言將其寫入了語言規(guī)格。ES6 亦是如此,第一次明確規(guī)定,所有 ECMAScript 的實(shí)現(xiàn),都必須部署“尾調(diào)用優(yōu)化”
。這就是說,ES6 中只要使用尾遞歸
,就不會(huì)發(fā)生棧溢出(或者層層遞歸造成的超時(shí)),相對(duì)節(jié)省內(nèi)存。
尾遞歸的實(shí)現(xiàn),往往需要改寫遞歸函數(shù),確保最后一步只調(diào)用自身。做到這一點(diǎn)的方法,就是把所有用到的內(nèi)部變量改寫成函數(shù)的參數(shù)。比如上面的例子,階乘函數(shù) factorial 需要用到一個(gè)中間變量 total ,那就把這個(gè)中間變量改寫成函數(shù)的參數(shù)。這樣做的缺點(diǎn)就是不太直觀,第一眼很難看出來,為什么計(jì)算 5 的階乘,需要傳入兩個(gè)參數(shù) 5 和 1 ?
兩個(gè)方法可以解決這個(gè)問題。方法一是在尾遞歸函數(shù)之外,再提供一個(gè)正常形式的函數(shù)。
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
上面代碼通過一個(gè)正常形式的階乘函數(shù) factorial ,調(diào)用尾遞歸函數(shù) tailFactorial ,看起來就正常多了。
函數(shù)式編程有一個(gè)概念,叫做柯里化(currying),意思是將多參數(shù)的函數(shù)轉(zhuǎn)換成單參數(shù)的形式。這里也可以使用柯里化。
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
上面代碼通過柯里化,將尾遞歸函數(shù) tailFactorial 變?yōu)橹唤邮芤粋€(gè)參數(shù)的 factorial 。
第二種方法就簡(jiǎn)單多了,就是采用 ES6 的函數(shù)默認(rèn)值。
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5) // 120
上面代碼中,參數(shù) total 有默認(rèn)值 1 ,所以調(diào)用時(shí)不用提供這個(gè)值。
總結(jié)一下,遞歸本質(zhì)上是一種循環(huán)操作。純粹的函數(shù)式編程語言沒有循環(huán)操作命令,所有的循環(huán)都用遞歸實(shí)現(xiàn),這就是為什么尾遞歸對(duì)這些語言極其重要。對(duì)于其他支持“尾調(diào)用優(yōu)化”的語言(比如 Lua,ES6),只需要知道循環(huán)可以用遞歸代替,而一旦使用遞歸,就最好使用尾遞歸。
ES6 的尾調(diào)用
優(yōu)化只在嚴(yán)格模式
下開啟,正常模式是無效的。
這是因?yàn)樵谡DJ较?,函?shù)內(nèi)部有兩個(gè)變量,可以跟蹤函數(shù)的調(diào)用棧。
尾調(diào)用優(yōu)化發(fā)生時(shí),函數(shù)的調(diào)用棧會(huì)改寫,因此上面兩個(gè)變量就會(huì)失真。嚴(yán)格模式禁用這兩個(gè)變量,所以尾調(diào)用模式僅在嚴(yán)格模式下生效。
function restricted() {
'use strict';
restricted.caller; // 報(bào)錯(cuò)
restricted.arguments; // 報(bào)錯(cuò)
}
restricted();
尾遞歸優(yōu)化只在嚴(yán)格模式下生效,那么正常模式下,或者那些不支持該功能的環(huán)境中,有沒有辦法也使用尾遞歸優(yōu)化呢?回答是可以的,就是自己實(shí)現(xiàn)尾遞歸優(yōu)化。
它的原理非常簡(jiǎn)單。尾遞歸之所以需要優(yōu)化,原因是調(diào)用棧太多,造成溢出,那么只要減少調(diào)用棧,就不會(huì)溢出。怎么做可以減少調(diào)用棧呢?就是采用“循環(huán)”
換掉“遞歸”
。
下面是一個(gè)正常的遞歸函數(shù)。
function sum(x, y) {
if (y > 0) {
return sum(x + 1, y - 1);
} else {
return x;
}
}
sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)
上面代碼中, sum 是一個(gè)遞歸函數(shù),參數(shù) x 是需要累加的值,參數(shù) y 控制遞歸次數(shù)。一旦指定 sum 遞歸 100000 次,就會(huì)報(bào)錯(cuò),提示超出調(diào)用棧的最大次數(shù)。
蹦床函數(shù)(trampoline)可以將遞歸執(zhí)行轉(zhuǎn)為循環(huán)執(zhí)行。
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
上面就是蹦床函數(shù)的一個(gè)實(shí)現(xiàn),它接受一個(gè)函數(shù) f 作為參數(shù)。只要 f 執(zhí)行后返回一個(gè)函數(shù),就繼續(xù)執(zhí)行。注意,這里是返回一個(gè)函數(shù),然后執(zhí)行該函數(shù),而不是函數(shù)里面調(diào)用函數(shù),這樣就避免了遞歸執(zhí)行,從而就消除了調(diào)用棧過大的問題。
然后,要做的就是將原來的遞歸函數(shù),改寫為每一步返回另一個(gè)函數(shù)。
function sum(x, y) {
if (y > 0) {
return sum.bind(null, x + 1, y - 1);
} else {
return x;
}
}
上面代碼中, sum 函數(shù)的每次執(zhí)行,都會(huì)返回自身的另一個(gè)版本。
現(xiàn)在,使用蹦床函數(shù)執(zhí)行 sum ,就不會(huì)發(fā)生調(diào)用棧溢出。
trampoline(sum(1, 100000))
// 100001
蹦床函數(shù)并不是真正的尾遞歸優(yōu)化,下面的實(shí)現(xiàn)才是。
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000)
// 100001
上面代碼中, tco 函數(shù)是尾遞歸優(yōu)化的實(shí)現(xiàn),它的奧妙就在于狀態(tài)變量 active 。默認(rèn)情況下,這個(gè)變量是不激活的。一旦進(jìn)入尾遞歸優(yōu)化的過程,這個(gè)變量就激活了。然后,每一輪遞歸 sum 返回的都是 undefined ,所以就避免了遞歸執(zhí)行;而 accumulated 數(shù)組存放每一輪 sum 執(zhí)行的參數(shù),總是有值的,這就保證了 accumulator 函數(shù)內(nèi)部的 while 循環(huán)總是會(huì)執(zhí)行。這樣就很巧妙地將“遞歸”改成了“循環(huán)”,而后一輪的參數(shù)會(huì)取代前一輪的參數(shù),保證了調(diào)用棧只有一層。
ES2017 允許函數(shù)的最后一個(gè)參數(shù)有尾逗號(hào)
(trailing comma)。
此前,函數(shù)定義和調(diào)用時(shí),都不允許最后一個(gè)參數(shù)后面出現(xiàn)逗號(hào)。
function clownsEverywhere(
param1,
param2
) { /* ... */ }
clownsEverywhere(
'foo',
'bar'
);
上面代碼中,如果在 param2
或 bar
后面加一個(gè)逗號(hào),就會(huì)報(bào)錯(cuò)。
如果像上面這樣,將參數(shù)寫成多行(即每個(gè)參數(shù)占據(jù)一行),以后修改代碼的時(shí)候,想為函數(shù) clownsEverywhere 添加第三個(gè)參數(shù),或者調(diào)整參數(shù)的次序,就勢(shì)必要在原來最后一個(gè)參數(shù)后面添加一個(gè)逗號(hào)。這對(duì)于版本管理系統(tǒng)來說,就會(huì)顯示添加逗號(hào)的那一行也發(fā)生了變動(dòng)。這看上去有點(diǎn)冗余,因此新的語法允許定義和調(diào)用時(shí),尾部直接有一個(gè)逗號(hào)。
function clownsEverywhere(
param1,
param2,
) { /* ... */ }
clownsEverywhere(
'foo',
'bar',
);
這樣的規(guī)定也使得,函數(shù)參數(shù)與數(shù)組和對(duì)象的尾逗號(hào)規(guī)則,保持一致了。
ES2019 對(duì)函數(shù)實(shí)例的 toString()
方法做出了修改。
toString() 方法返回函數(shù)代碼本身,以前會(huì)省略注釋和空格。
function /* foo comment */ foo () {}
foo.toString()
// function foo() {}
上面代碼中,函數(shù) foo 的原始代碼包含注釋,函數(shù)名 foo 和圓括號(hào)之間有空格,但是 toString() 方法都把它們省略了。
修改后的 toString() 方法,明確要求返回一模一樣的原始代碼。
function /* foo comment */ foo () {}
foo.toString()
// "function /* foo comment */ foo () {}"
JavaScript 語言的 try...catch
結(jié)構(gòu),以前明確要求 catch 命令后面必須跟參數(shù),接受 try 代碼塊拋出的錯(cuò)誤對(duì)象。
try {
// ...
} catch (err) {
// 處理錯(cuò)誤
}
上面代碼中, catch 命令后面帶有參數(shù) err 。
很多時(shí)候, catch 代碼塊可能用不到這個(gè)參數(shù)。但是,為了保證語法正確,還是必須寫。ES2019 做出了改變,允許 catch 語句省略參數(shù)。
try {
// ...
} catch {
// ...
}
更多建議: