ES6 以前,不能直接爲函數的參數指定默認值,只能採用變通的方法。javascript
function log(x, y) { y = y || 'World'; console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello World
上面代碼檢查函數log
的參數y
有沒有賦值,若是沒有,則指定默認值爲World
。這種寫法的缺點在於,若是參數y
賦值了,可是對應的布爾值爲false
,則該賦值不起做用。就像上面代碼的最後一行,參數y
等於空字符,結果被改成默認值。java
爲了不這個問題,一般須要先判斷一下參數y
是否被賦值,若是沒有,再等於默認值。web
if (typeof y === 'undefined') { y = 'World'; }
ES6 容許爲函數的參數設置默認值,即直接寫在參數定義的後面。數組
function log(x, y = 'World') { console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello
能夠看到,ES6 的寫法比 ES5 簡潔許多,並且很是天然。下面是另外一個例子。瀏覽器
function Point(x = 0, y = 0) { this.x = x; this.y = y; } const p = new Point(); p // { x: 0, y: 0 }
除了簡潔,ES6 的寫法還有兩個好處:首先,閱讀代碼的人,能夠馬上意識到哪些參數是能夠省略的,不用查看函數體或文檔;其次,有利於未來的代碼優化,即便將來的版本在對外接口中,完全拿掉這個參數,也不會致使之前的代碼沒法運行。ide
參數變量是默認聲明的,因此不能用let
或const
再次聲明。svg
function foo(x = 5) { let x = 1; // error const x = 2; // error }
上面代碼中,參數變量x
是默認聲明的,在函數體中,不能用let
或const
再次聲明,不然會報錯。函數
使用參數默認值時,函數不能有同名參數。fetch
// 不報錯 function foo(x, x, y) { // ... } // 報錯 function foo(x, x, y = 1) { // ... } // SyntaxError: Duplicate parameter name not allowed in this context
另外,一個容易忽略的地方是,參數默認值不是傳值的,而是每次都從新計算默認值表達式的值。也就是說,參數默認值是惰性求值的。優化
let x = 99; function foo(p = x + 1) { console.log(p); } foo() // 100 x = 100; foo() // 101
上面代碼中,參數p
的默認值是x + 1
。這時,每次調用函數foo
,都會從新計算x + 1
,而不是默認p
等於 100。
參數默認值能夠與解構賦值的默認值,結合起來使用。
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
上面代碼只使用了對象的解構賦值默認值,沒有使用函數參數的默認值。只有當函數foo
的參數是一個對象時,變量x
和y
纔會經過解構賦值生成。若是函數foo
調用時沒提供參數,變量x
和y
就不會生成,從而報錯。經過提供函數參數的默認值,就能夠避免這種狀況。
function foo({x, y = 5} = {}) { console.log(x, y); } foo() // undefined 5
上面代碼指定,若是沒有提供參數,函數foo
的參數默認爲一個空對象。
下面是另外一個解構賦值默認值的例子。
function fetch(url, { body = '', method = 'GET', headers = {} }) { console.log(method); } fetch('http://example.com', {}) // "GET" fetch('http://example.com') // 報錯
上面代碼中,若是函數fetch
的第二個參數是一個對象,就能夠爲它的三個屬性設置默認值。這種寫法不能省略第二個參數,若是結合函數參數的默認值,就能夠省略第二個參數。這時,就出現了雙重默認值。
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) { console.log(method); } fetch('http://example.com') // "GET"
上面代碼中,函數fetch
沒有第二個參數時,函數參數的默認值就會生效,而後纔是解構賦值的默認值生效,變量method
纔會取到默認值GET
。
做爲練習,請問下面兩種寫法有什麼差異?
// 寫法一 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]
一般狀況下,定義了默認值的參數,應該是函數的尾參數。由於這樣比較容易看出來,到底省略了哪些參數。若是非尾部的參數設置默認值,實際上這個參數是無法省略的。
// 例一 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
,就沒有觸發默認值。
指定了默認值之後,函數的length
屬性,將返回沒有指定默認值的參數個數。也就是說,指定了默認值後,length
屬性將失真。
(function (a) {}).length // 1 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2
上面代碼中,length
屬性的返回值,等於函數的參數個數減去指定了默認值的參數個數。好比,上面最後一個函數,定義了 3 個參數,其中有一個參數c
指定了默認值,所以length
屬性等於3
減去1
,最後獲得2
。
這是由於length
屬性的含義是,該函數預期傳入的參數個數。某個參數指定默認值之後,預期傳入的參數個數就不包括這個參數了。同理,後文的 rest 參數也不會計入length
屬性。
(function(...args) {}).length // 0
若是設置了默認值的參數不是尾參數,那麼length
屬性也再也不計入後面的參數了。
(function (a = 0, b, c) {}).length // 0 (function (a, b = 1, c) {}).length // 1
一旦設置了參數的默認值,函數進行聲明初始化時,參數會造成一個單獨的做用域(context)。等到初始化結束,這個做用域就會消失。這種語法行爲,在不設置參數默認值時,是不會出現的。
var x = 1; function f(x, y = x) { console.log(y); } f(2) // 2
上面代碼中,參數y
的默認值等於變量x
。調用函數f
時,參數造成一個單獨的做用域。在這個做用域裏面,默認值變量x
指向第一個參數x
,而不是全局變量x
,因此輸出是2
。
再看下面的例子。
let x = 1; function f(y = x) { let x = 2; console.log(y); } f() // 1
上面代碼中,函數f
調用時,參數y = x
造成一個單獨的做用域。這個做用域裏面,變量x
自己沒有定義,因此指向外層的全局變量x
。函數調用時,函數體內部的局部變量x
影響不到默認值變量x
。
若是此時,全局變量x
不存在,就會報錯。
function f(y = x) { let x = 2; console.log(y); } f() // ReferenceError: x is not defined
下面這樣寫,也會報錯。
var x = 1; function foo(x = x) { // ... } foo() // ReferenceError: x is not defined
上面代碼中,參數x = x
造成一個單獨做用域。實際執行的是let x = x
,因爲暫時性死區的緣由,這行代碼會報錯」x 未定義「。
若是參數的默認值是一個函數,該函數的做用域也遵照這個規則。請看下面的例子。
let foo = 'outer'; function bar(func = () => foo) { let foo = 'inner'; console.log(func()); } bar(); // outer
上面代碼中,函數bar
的參數func
的默認值是一個匿名函數,返回值爲變量foo
。函數參數造成的單獨做用域裏面,並無定義變量foo
,因此foo
指向外層的全局變量foo
,所以輸出outer
。
若是寫成下面這樣,就會報錯。
function bar(func = () => foo) { let foo = 'inner'; console.log(func()); } bar() // ReferenceError: foo is not defined
上面代碼中,匿名函數裏面的foo
指向函數外層,可是函數外層並無聲明變量foo
,因此就報錯了。
下面是一個更復雜的例子。
var x = 1; function foo(x, y = function() { x = 2; }) { var x = 3; y(); console.log(x); } foo() // 3 x // 1
上面代碼中,函數foo
的參數造成一個單獨做用域。這個做用域裏面,首先聲明瞭變量x
,而後聲明瞭變量y
,y
的默認值是一個匿名函數。這個匿名函數內部的變量x
,指向同一個做用域的第一個參數x
。函數foo
內部又聲明瞭一個內部變量x
,該變量與第一個參數x
因爲不是同一個做用域,因此不是同一個變量,所以執行y
後,內部變量x
和外部全局變量x
的值都沒變。
若是將var x = 3
的var
去除,函數foo
的內部變量x
就指向第一個參數x
,與匿名函數內部的x
是一致的,因此最後輸出的就是2
,而外層的全局變量x
依然不受影響。
var x = 1; function foo(x, y = function() { x = 2; }) { x = 3; y(); console.log(x); } foo() // 2 x // 1
利用參數默認值,能夠指定某一個參數不得省略,若是省略就拋出一個錯誤。
function throwIfMissing() { throw new Error('Missing parameter'); } function foo(mustBeProvided = throwIfMissing()) { return mustBeProvided; } foo() // Error: Missing parameter
上面代碼的foo
函數,若是調用的時候沒有參數,就會調用默認值throwIfMissing
函數,從而拋出一個錯誤。
從上面代碼還能夠看到,參數mustBeProvided
的默認值等於throwIfMissing
函數的運行結果(注意函數名throwIfMissing
以後有一對圓括號),這代表參數的默認值不是在定義時執行,而是在運行時執行。若是參數已經賦值,默認值中的函數就不會運行。
另外,能夠將參數默認值設爲undefined
,代表這個參數是能夠省略的。
function foo(optional = undefined) { ··· }
ES6 引入 rest 參數(形式爲...變量名
),用於獲取函數的多餘參數,這樣就不須要使用arguments
對象了。rest 參數搭配的變量是一個數組,該變量將多餘的參數放入數組中。
function add(...values) { let sum = 0; for (var val of values) { sum += val; } return sum; } add(2, 5, 3) // 10
上面代碼的add
函數是一個求和函數,利用 rest 參數,能夠向該函數傳入任意數目的參數。
下面是一個 rest 參數代替arguments
變量的例子。
// arguments變量的寫法 function sortNumbers() { return Array.prototype.slice.call(arguments).sort(); } // rest參數的寫法 const sortNumbers = (...numbers) => numbers.sort();
上面代碼的兩種寫法,比較後能夠發現,rest 參數的寫法更天然也更簡潔。
arguments
對象不是數組,而是一個相似數組的對象。因此爲了使用數組的方法,必須使用Array.prototype.slice.call
先將其轉爲數組。rest 參數就不存在這個問題,它就是一個真正的數組,數組特有的方法均可以使用。下面是一個利用 rest 參數改寫數組push
方法的例子。
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
從 ES5 開始,函數內部能夠設定爲嚴格模式。
function doSomething(a, b) { 'use strict'; // code }
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 } };
這樣規定的緣由是,函數內部的嚴格模式,同時適用於函數體和函數參數。可是,函數執行的時候,先執行函數參數,而後再執行函數體。這樣就有一個不合理的地方,只有從函數體之中,才能知道參數是否應該以嚴格模式執行,可是參數卻應該先於函數體執行。
// 報錯 function doSomething(value = 070) { 'use strict'; return value; }
上面代碼中,參數value
的默認值是八進制數070
,可是嚴格模式下不能用前綴0
表示八進制,因此應該報錯。可是實際上,JavaScript 引擎會先成功執行value = 070
,而後進入函數體內部,發現須要用嚴格模式執行,這時纔會報錯。
雖然能夠先解析函數體代碼,再執行參數代碼,可是這樣無疑就增長了複雜性。所以,標準索性禁止了這種用法,只要參數使用了默認值、解構賦值、或者擴展運算符,就不能顯式指定嚴格模式。
兩種方法能夠規避這種限制。第一種是設定全局性的嚴格模式,這是合法的。
'use strict'; function doSomething(a, b = a) { // code }
第二種是把函數包在一個無參數的當即執行函數裏面。
const doSomething = (function () { 'use strict'; return function(value = 42) { return value; }; }());
函數的name
屬性,返回該函數的函數名。
function foo() {} foo.name // "foo"
這個屬性早就被瀏覽器普遍支持,可是直到 ES6,纔將其寫入了標準。
須要注意的是,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 "