溫故而知新:JS變數提升與時間死區

圓圓圈圈圓圓的花花發表於2019-01-26

開始執行指令碼時,執行指令碼的第一步是編譯程式碼,然後再開始執行程式碼,如圖

溫故而知新:JS變數提升與時間死區

另外,在編譯優化方面來說,最開始時也並不是全部編譯好指令碼,而是當函式執行時,才會先編譯,再執行指令碼,如圖

溫故而知新:JS變數提升與時間死區

  • 編譯階段:經歷了詞法分析,語法分析生成AST,以及程式碼生成。並且在此階段,它只會掃描並且抽出環境中的宣告變數,宣告函式以便準備分配記憶體,所有的函式宣告和變數宣告都會被新增到名為Lexical Environment的JavaScript內部資料結構內的記憶體中。因此,它們可以在原始碼中實際宣告之前使用。但是,Javascript只會儲存函式宣告和變數宣告在記憶體,並不會儲存他們的值
  • 執行階段:給變數x賦值,首先詢問記憶體你這有變數x嗎,如果有,則給變數x賦值,如果沒有則建立變數x並且給它賦值。

變數提升

如下圖,左邊灰色塊區域,是演示函式執行前的編譯階段,先抽出所有宣告變數和宣告函式,並進行記憶體分配。然後再開始執行程式碼,在執行第一行程式碼的時候,若是變數a存在於記憶體中,則直接給變數a賦值。而執行到第二行時,變數b並沒有在記憶體中,則會建立變數b並給它賦值。

溫故而知新:JS變數提升與時間死區

Lexical enviroment是一種包含識別符號變數對映的資料結構

LexicalEnviroment = {
  Identifier: <value>,
  Indentifier: <function object>
}
複製程式碼

簡而言之,Lexical enviroment就是程式執行過程中變數和函式存在的地方。

let,const變數

console.log(a)
let a = 3;
複製程式碼

輸出

ReferenceError: a is not defined
複製程式碼

所以let和const變數並不會被提升嗎?

這個答案會比較複雜。所有的宣告(function, var, let, const and class)在JavaScript中都會被提升,然而var宣告被undefined值初始化,但是letconst宣告的值仍然未被初始化。

它們僅僅只在Javascript引擎執行期間它們的詞法繫結被執行在才會被初始化。這意味著引擎在原始碼中宣告它的位置計算其值之前,你無法訪問該變數。這就是我們所說的時間死區,即變數建立和初始化之間的時間,我們無法訪問該變數。

如果JavaScript引擎仍然無法在宣告它們的行中找到let或者const的值,它將為它們分配undefined值或返回錯誤值(在const的情況下會返回錯誤值)。

溫故而知新:JS變數提升與時間死區

let a;
console.log(a); // outputs undefined
a = 5;
複製程式碼

在編譯階段,JavaScript引擎遇到變數a並將它儲存在lexical enviroment,但是因為它是一個let變數,所以引擎不會為它初始化任何值。所以,在編譯階段,lexical enviroment看起來像下面這樣。

// 編譯階段
lexicalEnvironment = {
  a: <uninitialized>
}
複製程式碼

現在如果我們嘗試在宣告它之前訪問該變數,JavaScript引擎將會嘗試從詞法環境中拿到這個變數的值,因為這個變數未被初始化,它將丟擲一個引用錯誤。

在執行期間,當引擎到達了變數宣告的行,它將試圖執行它的繫結,因為該變數沒有與之關聯的值,因此它將為其賦值為unedfined

// 執行階段
lexicalEnviroment = {
  a: undefined
}
複製程式碼

之後,undefined將會被列印到控制檯,然後將值5賦值給變數a,lexical enviroment中變數a的值也會從undefined更新為5

functionn foo() {
  console.log(a)
}

let a = 20;

foo(); 
複製程式碼
function foo() {
  console.log(a): // ReferenceError: a is not defined
}
foo();
let a = 20;
複製程式碼

溫故而知新:JS變數提升與時間死區

Class Declaration

就像letconst宣告一樣,class在JavaScript中也會被提升,並且和let,const一樣,知道執行之前,它們都會保持uninitialized。因此它們同樣會受到Temporal Deal Zone(時間死區)的影響。例如

let peter = new Person('Peter', 25); // ReferenceError: Person is not defined

console.log(peter);

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
複製程式碼

因此要訪問class,必須先宣告它

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

let peter = new Person('Peter', 25); 
console.log(peter);
// Person { name: 'Peter', age: 25 }
複製程式碼

所以在編譯階段,上面程式碼的lexical environment(詞法環境)將如下所示:

lexicalEnvironment = {
  Person: <uninitialized>
}
複製程式碼

當引擎執行class宣告時,它將使用值初始化類。

lexicalEnvironment = {
  Person: <Person object>
}
複製程式碼

提升Class Expressions

let peter = new Person('Peter', 25);
console.log(peter);
let Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
複製程式碼

溫故而知新:JS變數提升與時間死區

let peter = new Person('Peter', 25); 
console.log(peter);
var Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
複製程式碼

溫故而知新:JS變數提升與時間死區

所以現在我們知道在提升過程中我們的程式碼並沒有被JavaScript引擎實際移動。正確理解提升機制將有助於避免因變數提升而產生的任何未來錯誤和混亂。為了避免像未定義的變數或引用錯誤一樣可能產生的副作用,請始終嘗試將變數宣告在各自作用域的頂部,並始終嘗試在宣告變數時初始化變數。

Hoisting in Modern JavaScript — let, const, and var

相關文章