- 原文地址:Unpacking hoisting
- 原文做者:Dr. Axel Rauschmayer
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:DEARPORK
- 校對者:csming1995, Jalan
引用 ES6 規範做者 Allen Wirfs-Brock 一條最近的推特:html
變量提高是一個陳舊且使人困惑的術語。甚至在 ES6 以前:變量提高的意思到底是「提高至當前做用域頂部」仍是「從嵌套的代碼塊中提高到最近的函數或腳本做用域中」?仍是二者都有?前端
受 Allen 啓發,本文提出了一種不一樣的方法來描述變量聲明。android
我建議將聲明分爲兩個方面:ios
下面的表格總結了不一樣的聲明如何處理這兩個方面。「Duplicates」表示一個變量名是否容許在同一做用域聲明兩次。「Global prop.」表示一個在 script 標籤(模塊的前身)中的聲明,在全局做用域中被執行時,是否會向全局對象添加屬性。TDZ 意思是暫時死區(咱們稍後解釋)。函數聲明在嚴格模式下是塊做用域(例如在模塊內部),但在非嚴格模式下是函數做用域。git
如下部分更加詳細地描述了其中一些結構的行爲。程序員
const
和 let
:暫時死區對於 JavaScript,TC39 須要決定若是在聲明以前訪問其直接做用域中的常量會發生什麼:github
{
console.log(x); // 這裏會發生什麼?
const x;
}
複製代碼
一些可能的方案是:後端
undefined
。方案(1)被否決,由於這種方案在該語言中沒有先例。所以這對於 JavaScript 程序員並不直觀。ide
方案(2)被否決,由於這樣 x
將不是一個常量 —— 在聲明前和聲明後它將擁有不一樣的值。函數
let
與 const
同樣使用了方案(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);
複製代碼
若是你用 const
或 let
聲明一個函數,那麼它就不會被提早激活:在下面的例子中,你只能在 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
:變量提高(部分提早激活)var
是 const
和 let
(如今更建議使用這兩種方式)以前一種更老的聲明變量的方式。考慮如下 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 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。