揭祕變量提高

原文:2ality.com/2019/05/unp…html

譯者:前端小智前端

爲了保證可讀性,本文采用意譯而非直譯。git

想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!github

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

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

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

1. 聲明:做用域與激活

能夠將聲明分爲兩個方面:spa

  • 做用域:在哪裏能夠看到聲明的變量? 這是一個靜態特徵。
  • 激活:我何時能夠訪問變量? 這是一個動態特徵:有些變量只要咱們進入其做用域,就能夠訪問。 有的,咱們必須等到執行到它們的聲明。

下表總結了不一樣聲明的方式如何處理上述兩個方面。code

**「Duplicates」**描述是否能夠在同一做用域內聲明兩次。cdn

**「Global prop.」**表示一個在 script 中的聲明,當全局做用域中被執行時,是否會向全局對象添加屬性。

TDZ 表示暫時性死區(稍後解釋)。 函數聲明在嚴格模式下是塊做用域的(例如在模塊內部),但在非嚴格模式下是函數做用域。

2. const 和 let :暫時性死區

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

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

主要有兩種種狀況:

  1. 打印 undefined

  2. 報錯

第一種不會出現,由於 x 是一個常量,若是打印 undefined,在聲明前和聲明後它將擁有不一樣的值,x 就不是常量了。

let 和 const 都會出現第二種狀況,就是會報錯。進入變量做用域與執行聲明之間的這段時間被稱爲該變量的 臨時死區(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聲明以前使用 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() {
   console.log(MY_STR)
}
複製代碼

上述會報錯:

若是你在 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 :變量提高(部分提早激活)

var是在constlet以前聲明變量的一種較老的方法。考慮下面的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);
}
複製代碼

交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

相關文章
相關標籤/搜索