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


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

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

ES5及以下手動處理默認值

在ES6默認值特性出現前,手動處理默認值有幾種方式:

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檢測:

if (typeof level == 'undefined') {
  level = 'warning';
}

有時也可以檢查arguments.length

if (arguments.length == 1) {
  level = 'warning';
}

這些方法都可以很好的工作,但都過於手動且缺少抽象。ES6規范了直接在函數頭定義參數默認值的句法結構。

ES6默認值:基本例子

默認參數特性在很多語言中普遍存在,其基本形式可能大多數開發者都比較熟悉:

function log(message, level = 'warning') {
  console.log(level, ': ', message);
}

log('low memory'); // warning: low memory
log('out of memory', 'error'); // error: out of memory

參數默認值使用方便且毫無違和感。接下來讓我們深入細節實現,掃除默認參數所帶來的一些困惑。

實現細節

以下為一些函數默認參數的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!
來上例的y輸出結果看起來像是1,但實際上是2,不是1。原因是參數中的x與全局的x不同。由於默認值在函數調用時求值,所以當賦值=x時,x已經在內部作用域決定了,引用的是參數x本身。也就是說,參數x被全局的同名變量遮蔽,所以每次默認值中訪問x時,實際訪問到的是參數中的x

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

ES6提到所謂的TDZ(暫存死區),意指這樣的程序區域:初始化前的變量或參數不能被訪問。

考慮到對於參數,不能將自己作為默認值:

var x = 1;

function foo(x = x) { // throws!
  ...
}
賦值=x正如我們上面提到的那樣,x會被解釋為參數級作用域中的x,而全局的x會被遮蔽。但是,x位於TDZ,在初始化前不能被訪問。因此,它不能自己初始化自己。

注意,上面之前的例子中的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默認值(它們全部只創建了一個作用域,也就是函數作用域)。默認值顯然是一個有用的特性,它使得我們的代碼更加優雅和明確。

作者


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM