失效的詞法作用域查詢規則?

,發表於2017-09-21

今天在工作中遇到這樣一個問題:

var a = 1;  //一個常量值,本意是在後面多個函式中引用該常量值

function test1() {
    var a = a;
    console.log(a);
}

我想當然的認為函式 test1() 會列印出 1,然而結果是 undefined。後來我意識到,可能是 JS 中的“變數提升”機制在作怪。

在《你不知道的 JavaScript (上卷)》中,作者對“變數提升”機制進行了剖析。在 JS 的編譯階段(是的,JS也需要編譯),首先會蒐集變數與函式宣告。無論在什麼位置進行的宣告,都會在這一階段收集到,並且放置到最前面。

如下面程式碼所示:

function test() {
    a = 1;
    console.log(a);
    var a;
}

test(); //1
console.log(a); //ReferenceError

函式 test 相當於:

function test() {
    var a;
    a = 1;
    console.log(a);
}

所以呢,在 test1 中,第一條語句相當於:

var a;
a = a;

由於在該函式作用域內已經可以通過向左查詢查詢到變數 a,那麼就不需要向外面的作用域去尋找了。雖然找到了名稱為 a 的變數,但是沒有給它進行初始化,因此第二句賦值語句的結果是 a === undefined

而對於使用 let 關鍵字宣告的變數來說,不存在變數提升問題,並且在函式開始到該宣告之前,都不允許對該變數進行使用,這段區域被稱為“暫時死區”(TDZ)。

關於暫時死區,參考如下解釋

當程式的控制流程在新的作用域(module, function或block作用域)進行例項化時,在此作用域中的用let/const宣告的變數會先在作用域中被建立出來,但因此時還未進行詞法繫結,也就是對宣告語句進行求值運算,所以是不能被訪問的,訪問就會丟擲錯誤。所以在這執行流程一進入作用域建立變數,到變數開始可被訪問之間的一段時間,就稱之為TDZ(暫時死區)。

簡單來說就是,在進入變數所宣告的作用域到變數初始化之前的程式執行時間裡,變數都是不可用的。

例如:

function test2() {
    let a = a;
    console.log(a); //ReferenceError
}

在 ECMAScript 規範的 13.3.1 章節說明了變數是如何進行初始化的。

所以test2並不等價於:

function test2_2() {
    let a;
    a = a;
    console.log(a); //undefined
}

test2 中,雖然編譯器會首先建立變數 a,但是不會對其進行初始化,而是試圖使用 a 來對其進行初始化。但是因為彼時 a 處於 TDZ 中,所以會丟擲 ReferenceError 錯誤。test2_2 則不然,語句 let a; 會使編譯器建立變數 a,同時會賦予它 undefined 值。

同理可以解釋:

var a = 1;

function test2(a) {
    let a = a;
    console.log(a);
}

test2(a);    //ReferenceError

而對於

var a = 1;
function test1(a) {
    var a = a;
    console.log(a); //1
}

來說,由於形式引數 a 與變數宣告同處於一個作用域內, var a 相當於重複宣告。例如:

var a = 1;
var a;
console.log(a); //1

參考資料:

  1. 《你不知道的 JavaScript(上卷)》第4章
  2. https://segmentfault.com/a/1190000008213835
  3. https://www.ecma-international.org/ecma-262/6.0/#sec-let-and-const-declarations

相關文章