在這個簡短的筆記中咱們聊一聊ES6的又一特性:帶默認值的函數參數。正如咱們即將看到的,有些較爲微妙的CASE。javascript
在ES6默認值特性出現前,手動處理默認值有幾種方式:java
function log(message, level) { level = level || 'warning'; console.log(level, ': ', message); } log('low memory'); // warning: low memory log('out of memory', 'error'); // error: out of memory
爲了處理參數未傳遞的狀況,咱們常看到typeof檢測:es6
if (typeof level == 'undefined') { level = 'warning'; }
有時也能夠檢查arguments.length編程
if (arguments.length == 1) { level = 'warning'; }
這些方法均可以很好的工做,但都過於手動且缺乏抽象。ES6規範了直接在函數頭定義參數默認值的句法結構。閉包
默認參數特性在不少語言中廣泛存在,其基本形式可能大多數開發者都比較熟悉:app
function log(message, level = 'warning') { console.log(level, ': ', message); } log('low memory'); // warning: low memory log('out of memory', 'error'); // error: out of memory
參數默認值使用方便且毫無違和感。接下來讓咱們深刻細節實現,掃除默認參數所帶來的一些困惑。ecmascript
如下爲一些函數默認參數的ES6實現細節。編程語言
相對其它一些語言(如Python)在定義時一次性對默認值求值,ECMAScript在每次函數調用的執行期纔會計算默認值。這種設計是爲了不在複雜對象做爲默認值使用時引起一些困惑。接下來請看下面Python的例子:函數
def foo(x = []): x.append(1) return x # 咱們能夠看到默認值在函數定義時只建立了一次 # 而且存於函數對象的屬性中 print(foo.__defaults__) # ([],) foo() # [1] foo() # [1, 1] foo() # [1, 1, 1] print(foo.__defaults__) # ([1, 1, 1],)
爲了不這種現象,Python開發者一般把默認值定義爲None
,而後爲這個值作顯式檢查:優化
def foo(x = None): if x is None: x = [] x.append(1) print(x) print(foo.__defaults__) # (None,) foo() # [1] foo() # [1] foo() # [1] print(foo.__defaults__) # ([None],)
就目前,很好很直觀。接下來你會發現,若不瞭解默認值的工做方式,ES5語義上會產生一些困惑。
來看下面的例子:
var x = 1; function foo(x, y = x) { console.log(y); } foo(2); // 2, 不是 1!
來上例的輸出結果看起來像是,但其實是,不是。緣由是參數中的與全局的不一樣。因爲默認值在函數調用時求值,因此當賦值時,已經在內部做用域決定了,引用的是參數自己。也就是說,參數被全局的同名變量遮蔽,因此每次默認值中訪問時,實際訪問到的是參數中的。y121xx=xxxxxx
ES6提到所謂的TDZ(暫存死區),意指這樣的程序區域:初始化前的變量或參數不能被訪問。
考慮到對於參數,不能將本身做爲默認值:
var x = 1; function foo(x = x) { // throws! ... }
賦值正如咱們上面提到的那樣,會被解釋爲參數級做用域中的,而全局的會被遮蔽。可是,位於TDZ,在初始化前不能被訪問。所以,它不能本身初始化本身。=xxxxx
注意,上面以前的例子中的y
倒是合法的,由於x在以前已經初始化了(隱式的默認值undefined
)。因此咱們再看下:
function foo(x, y = x) { // OK ... }
這樣不會出問題,由於在ECMAScript中,參數的解析順序是從左到右,因此在對y
求值時x
已經可用。
咱們提到過參數是和」內部做用域」相關的,在ES5中咱們可假設這個」內部做用域」就是函數做用域。但更復雜的狀況:多是函數的做用域,或者,一個只爲存儲參數綁定的當即做用域。讓咱們繼續探索。
事實上,對於一些參數(至少一個)有默認值的狀況,ES6會定義一個當即做用域來存儲這些參數,而且這個做用域並不會與函數做用域共享。在這方面這是ES6與ES5的一個主要區別。有點暈?沒關係,看下例子你就懂。
var x = 1; function foo(x, y = function() { x = 2; }) { var x = 3; y(); // 局部變量`x`會被改寫乎? console.log(x); // no, 依然是3, 不是2 } foo(); // 並且外層的`x`也未變化 console.log(x); // 1
在這個例子中,咱們有三個做用域:全局環境、參數環境、函數環境:
: {x: 3} // 函數 -> {x: undefined, y: function() { x = 2; }} // 參數 -> {x: 1} // 全局
如今咱們應該清楚了,看成爲參數的函數對象y
執行時,它內部的x
會被就近解析(也就是上面說的參數環境),函數做用域對其並不可見。
若是咱們想把ES6代碼編譯到ES5,而且須要搞清楚這個當即做用域到底是什麼樣的,咱們能夠獲得像這樣的東東:
// ES6 function foo(x, y = function() { x = 2; }) { var x = 3; y(); // 局部變量`x`會被改寫嗎? console.log(x); // no, 依然是3, 不是2 } // 編譯到ES5 function foo(x, y) { // 設置默認參數 if (typeof y == 'undefined') { y = function() { x = 2; }; // 如今弄清楚了,將會更新參數中的`x` } return function() { var x = 3; // 這裏的`x`是函數做用域的 y(); console.log(x); }.apply(this, arguments); }
設計參數級做用域的目的到底是什麼?爲何不能像ES5那樣能夠訪問到函數做用域中的變量?緣由:參數默認值是函數時,其函數體內的同名變量不該該影響被捕獲閉包中的同名綁定。
例:
var x = 1; function foo(y = function() { return x; }) { // 捕獲 `x` var x = 2; return y(); } foo(); // 正確的應該是 1, 不是 2
若是咱們在函數體內建立函數y
,它內部的return x
中的x
會捕獲函數做用域下的x
,也就是2
。可是,很明顯,參數y
函數中的x
應該捕獲到全局的x
,也就是1
(除非被同名參數遮蔽)。
同時,這裏不能在外部做用域下建立函數,由於這樣就意味着沒法訪問這個函數的參數了,因此咱們應該這樣作:
var x = 1; function foo(y, z = function() { return x + y; }) { // 如今全局`x` 和參數`y`均在參數`z`函數中可見 var x = 3; return z(); } foo(1); // 2, 不是 4
上面的描述的默認值工做方式,在語義上與最開始咱們手動實現默認值徹底不一樣,例:
var x = 1; function foo(x, y) { if (typeof y == 'undefined') { y = function() { x = 2; }; } var x = 3; y(); // 局部變量`x`會被改寫麼? console.log(x); // 此次被改寫了!輸出2 } foo(); // 而全局的`x`仍然未變化 console.log(x); // 1
這個事實頗有趣:若是函數無默認值,它不會建立這個當即做用域,而且與函數環境共享參數綁定,也就是像ES5那樣處理。這也是爲何說是『有條件的參數當即做用域』
爲何會這樣?爲何不每次建立參數級做用域?只是爲了優化?非也非也。這麼作的緣由實際上是爲了向後兼容ES5:上面手動模擬默認值機制的代碼應該更新函數體的x
(也就是參數x
,在相同做用域下實際是同一個變量被重複聲明,一次是參數定義,一次是局部變量`x`)。
另外,須要注意到只有變量和函數容許重複聲明,而用let/const重複聲明參數是不容許的:
function foo(x = 5) { let x = 1; // error const x = 2; // error }
另一個有趣的事情是:是否默認值會被應用將取決於初始值也就是傳參是否爲undefined
(在進入上下文時被賦值)。例:
function foo(x, y = 2) { console.log(x, y); } foo(); // undefined, 2 foo(1); // 1, 2 foo(undefined, undefined); // undefined, 2 foo(1, undefined); // 1, 2
一般狀況下在一些編程語言中,帶默認值參數會在必選參數的後面,可是,在JavaScript中容許下面的構造:
function foo(x = 2, y) { console.log(x, y); } foo(1); // 1, undefined foo(undefined, 1); // 2, 1
另外一個默認值涉及到的地方是解構組件的默認值。解構賦值的討論不在本文中詳述,但咱們能夠看一些簡單的例子。對於在函數參數中使用解構的處理,與上面描述過的默認值處理相同:也就是必要時會建立兩個做用域:
function foo({x, y = 5}) { console.log(x, y); } foo({}); // undefined, 5 foo({x: 1}); // 1, 5 foo({x: 1, y: 2}); // 1, 2
固然,解構的默認值更加通用,不僅在函數參數默認值中可用:
var {x, y = 5} = {x: 1}; console.log(x, y); // 1, 5
但願這個簡短的記錄能幫助你們理解ES6中的默認值特性的細節。須要注意的是,因爲這個」第二做用域」是最近才加入到規範草稿中的,所以截至本文撰寫時(2014年8月21日),沒有任何引擎正確的實現了ES6默認值(它們所有隻建立了一個做用域,也就是函數做用域)。默認值顯然是一個有用的特性,它使得咱們的代碼更加優雅和明確。