ES6筆記之參數默認值(譯)

  • 原文連接:http://dmitrysoshnikov.com/
  • 原文做者:Dmitry Soshnikov
  • 譯者作了少許補充。這樣的的文字是譯者加的,能夠選擇忽略。
  • 做者微博:@Bosn

在這個簡短的筆記中咱們聊一聊ES6的又一特性:帶默認值的函數參數。正如咱們即將看到的,有些較爲微妙的CASE。javascript

ES5及如下手動處理默認值

在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規範了直接在函數頭定義參數默認值的句法結構。閉包

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

參數的TDZ(Temporal Dead Zone,暫存死區)

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會被就近解析(也就是上面說的參數環境),函數做用域對其並不可見。

編譯到ES5

若是咱們想把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的檢測

另一個有趣的事情是:是否默認值會被應用將取決於初始值也就是傳參是否爲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默認值(它們所有隻建立了一個做用域,也就是函數做用域)。默認值顯然是一個有用的特性,它使得咱們的代碼更加優雅和明確。

做者

相關文章
相關標籤/搜索