原文:2ality.com/2019/05/unp…html
譯者:前端小智前端
爲了保證可讀性,本文采用意譯而非直譯。git
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!github
引用 ES6 規範做者 Allen Wirfs-Brock一條最近的推特:ide
變量提高是一個陳舊且使人困惑的術語。甚至在 ES6 以前:變量提高的意思到底是「提高至當前做用域頂部」仍是「從嵌套的代碼塊中提高到最近的函數或腳本做用域中」?仍是二者都有?函數
受 Allen 啓發,本文提出了一種不一樣的方法來描述變量聲明。學習
能夠將聲明分爲兩個方面:spa
下表總結了不一樣聲明的方式如何處理上述兩個方面。code
**「Duplicates」**描述是否能夠在同一做用域內聲明兩次。cdn
**「Global prop.」**表示一個在 script 中的聲明,當全局做用域中被執行時,是否會向全局對象添加屬性。
TDZ
表示暫時性死區(稍後解釋)。 函數聲明在嚴格模式下是塊做用域的(例如在模塊內部),但在非嚴格模式下是函數做用域。
對於JavaScript,TC39 須要決定若是在聲明以前訪問其直接做用域中的常量會發生什麼:
{
console.log(x); // 這裏會發生什麼?
const x;
}
複製代碼
主要有兩種種狀況:
打印 undefined
報錯
第一種不會出現,由於 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);
複製代碼
若是用 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() {
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
是在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);
}
複製代碼
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!
關注公衆號,後臺回覆福利,便可看到福利,你懂的。