[譯] 揭祕變量提高

引用 ES6 規範做者 Allen Wirfs-Brock 一條最近的推特html

變量提高是一個陳舊且使人困惑的術語。甚至在 ES6 以前:變量提高的意思到底是「提高至當前做用域頂部」仍是「從嵌套的代碼塊中提高到最近的函數或腳本做用域中」?仍是二者都有?前端

受 Allen 啓發,本文提出了一種不一樣的方法來描述變量聲明。android

聲明:做用域與激活

我建議將聲明分爲兩個方面:ios

  • 做用域:在哪能夠看到一個聲明的實體?這是一個靜態特徵。
  • 激活:我什麼時候能夠訪問實體?這是一個動態特徵:有的實體在咱們進入他們做用域的時候就能夠被訪問,其他的咱們必須等待代碼執行到它們的聲明。

下面的表格總結了不一樣的聲明如何處理這兩個方面。「Duplicates」表示一個變量名是否容許在同一做用域聲明兩次。「Global prop.」表示一個在 script 標籤(模塊的前身)中的聲明,在全局做用域中被執行時,是否會向全局對象添加屬性。TDZ 意思是暫時死區(咱們稍後解釋)。函數聲明在嚴格模式下是塊做用域(例如在模塊內部),但在非嚴格模式下是函數做用域。git

如下部分更加詳細地描述了其中一些結構的行爲。程序員

constlet:暫時死區

對於 JavaScript,TC39 須要決定若是在聲明以前訪問其直接做用域中的常量會發生什麼:github

{
  console.log(x); // 這裏會發生什麼?
  const x;
}
複製代碼

一些可能的方案是:後端

  1. 該變量名在包圍當前做用域的做用域中解析。
  2. 你會獲得 undefined
  3. 報錯。

方案(1)被否決,由於這種方案在該語言中沒有先例。所以這對於 JavaScript 程序員並不直觀。ide

方案(2)被否決,由於這樣 x 將不是一個常量 —— 在聲明前和聲明後它將擁有不一樣的值。函數

letconst 同樣使用了方案(3),因此它們工做方式類似而且很容易在它們之間切換。

進入變量做用域與執行聲明之間的這段時間被稱爲該變量的暫時死區(TDZ):

  • 在此期間,該變量被認爲是未初始化的(就好像它有一個特殊的值)。
  • 若是你訪問一個未初始化的變量,你會獲得一個 ReferenceError
  • 一旦你執行到了變量聲明,這個變量將被設置爲初始化的值(經過賦值符號指定)或者 undefined —— 若是沒有初始化的話。

如下代碼闡釋了暫時死區:

if (true) { // 進入 `tmp` 的做用域,TDZ 開始
  // `tmp` 未被初始化:
  assert.throws(() => (tmp = 'abc'), ReferenceError);
  assert.throws(() => console.log(tmp), ReferenceError);

  let tmp; // TDZ 結束
  assert.equal(tmp, undefined);
}
複製代碼

下一個例子代表暫時死區是真的暫時的(與時間有關):

if (true) { // 進入 `myVar` 做用域,TDZ 開始
  const func = () => {
    console.log(myVar); // 稍後執行
  };

  // 咱們在 TDZ 中:
  // 訪問 `myVar` 形成 `ReferenceError`

  let myVar = 3; // TDZ 結束
  func(); // OK,在 TDZ 外調用
}
複製代碼

即便 func() 聲明位於 myVar 聲明以前且使用了該變量,咱們仍然能夠調用 func()。可是咱們必須等到 myVar 的暫時死區結束以後。

函數聲明與提早激活

不管一個函數聲明在做用域內的什麼位置,它都會在進入做用域時執行。這使你能夠在 foo() 函數聲明前調用它。

assert.equal(foo(), 123); // OK
function foo() { return 123; }
複製代碼

提早激活 foo() 意味着上述代碼至關於:

function foo() { return 123; }
assert.equal(foo(), 123);
複製代碼

若是你用 constlet 聲明一個函數,那麼它就不會被提早激活:在下面的例子中,你只能在 bar() 聲明後調用它。

assert.throws(
  () => bar(), // 聲明前
  ReferenceError);

const bar = () => { return 123; };

assert.equal(bar(), 123); // 聲明後
複製代碼

未提早激活的提早調用

即便函數 g() 並未提早激活,它仍能夠被前面的函數 f()(在同一做用域中)調用 —— 只要咱們遵照如下規則:f() 必須在聲明 g() 以後調用。

const f = () => g();
const g = () => 123;

// 咱們在 g() 聲明後調用 f():
assert.equal(f(), 123);
複製代碼

模塊中的函數一般在模塊體執行完後才被調用。因此在模塊中,你不多須要擔憂函數的順序。

最後,注意提早激活是怎樣自動執行以維持上述規則的:當進入一個做用域時,在任何函數被調用前,全部的函數聲明都會被先執行。

提早激活的一個陷阱

若是你依賴提早激活在聲明前調用一個函數,那你須要注意它並不能訪問未提早激活的數據。

funcDecl();

const MY_STR = 'abc';
function funcDecl() {
  assert.throws(
    () => MY_STR,
    ReferenceError);
}
複製代碼

若是你在 MY_STR 聲明以後調用 funcDecl() 問題將不復存在。

提早激活的利弊

咱們已經看到提早激活有一個陷阱,你能夠在不使用它的狀況下得到大部分好處。所以,最好避免提早激活。但我對此說法並不是十分認同,如前所述,我常用函數聲明,由於我喜歡它們的語法。

類聲明不會提早激活

類聲明不會提早激活:

assert.throws(
  () => new MyClass(),
  ReferenceError);

class MyClass {}

assert.equal(new MyClass() instanceof MyClass, true);
複製代碼

爲何呢?看看下面的類聲明:

class MyClass extends Object {}
複製代碼

extends 是可選的。它的操做數是個表達式。所以,你能夠這樣作:

const identity = x => x;
class MyClass extends identity(Object) {}
複製代碼

計算這樣的表達式必須在它被引用的地方完成。其它行爲都會令人困惑。這解釋了爲何類聲明不提早激活。

var:變量提高(部分提早激活)

varconstlet(如今更建議使用這兩種方式)以前一種更老的聲明變量的方式。考慮如下 var 聲明。

var x = 123;
複製代碼

這個聲明包含兩個部分:

  • 聲明 var x:用 var 聲明的變量的做用域是最裏面的包圍函數,而不是最裏面的包圍塊,就像大多數其餘聲明同樣。這樣的變量在它做用域的開頭就已經激活並以 undefined 初始化。
  • 賦值 x = 123:賦值老是在適當位置執行。

如下代碼演示了 var

function f() {
  // 部分提早激活:
  assert.equal(x, undefined);
  if (true) {
    var x = 123;
    // 賦值已經執行
    assert.equal(x, 123);
  }
  // 做用域爲函數做用域,非塊級做用域。
  assert.equal(x, 123);
}
複製代碼

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索