深入理解JS:var、let、const的異同

forcheng發表於2020-06-02

目錄

  • 序言
  • var 與 let 的區別
    • 作用域
    • 重複宣告
    • 繫結全域性物件
    • 變數提升與暫存死區
  • let 與 const 異同
  • 參考

1.序言

var、let 和 const 都是 JavaScript 中用來宣告變數的關鍵字,並且 let 和 const 關鍵字是在 ES6 中才新增的。既然都是用來宣告變數的,那它們之間有什麼區別呢?讓我們來一探究竟。


2.var 與 let 的區別

(1)作用域

用 var 宣告的變數的作用域是它當前的執行上下文,即如果是在任何函式外面,則是全域性執行上下文,如果在函式裡面,則是當前函式執行上下文。換句話說,var 宣告的變數的作用域只能是全域性或者整個函式塊的。

而 let 宣告的變數的作用域則是它當前所處程式碼塊,即它的作用域既可以是全域性或者整個函式塊,也可以是 if、while、switch等用{}限定的程式碼塊。

另外,var 和 let 的作用域規則都是一樣的,其宣告的變數只在其宣告的塊或子塊中可用。

示例程式碼:

function varTest() {
  var a = 1;

  {
    var a = 2; // 函式塊中,同一個變數
    console.log(a); // 2
  }

  console.log(a); // 2
}

function letTest() {
  let a = 1;

  {
    let a = 2; // 程式碼塊中,新的變數
    console.log(a); // 2
  }

  console.log(a); // 1
}

varTest();
letTest();

從上述示例中可以看出,let 宣告的變數的作用域可以比 var 宣告的變數的作用域有更小的限定範圍,更具靈活。


(2)重複宣告

var 允許在同一作用域中重複宣告,而 let 不允許在同一作用域中重複宣告,否則將丟擲異常。

var 相關示例程式碼:

var a = 1;
var a = 2;

console.log(a) // 2

function test() {
  var a = 3;
  var a = 4;
  console.log(a) // 4
}

test()

let 相關示例程式碼:

if(false) {
  let a = 1;
  let a = 2; // SyntaxError: Identifier 'a' has already been declared
}
switch(index) {
  case 0:
    let a = 1;
  break;

  default:
    let a = 2; // SyntaxError: Identifier 'a' has already been declared
    break;
}

從上述示例中可以看出,let 宣告的重複性檢查是發生在詞法分析階段,也就是在程式碼正式開始執行之前就會進行檢查。


(3)繫結全域性物件

var 在全域性環境宣告變數,會在全域性物件裡新建一個屬性,而 let 在全域性環境宣告變數,則不會在全域性物件裡新建一個屬性。

示例程式碼:

var foo = 'global'
let bar = 'global'

console.log(this.foo) // global
console.log(this.bar) // undefined

那這裡就一個疑問, let 在全域性環境宣告變數不在全域性物件的屬性中,那它是儲存在哪的呢?

var foo = 'global'
let bar = 'global'

function test() {}

console.dir(test)

在Chrome瀏覽器的控制檯中,通過執行上述程式碼,檢視 test 函式的作用域鏈,其結果如圖:

test 函式的作用域鏈

由上圖可知,let 在全域性環境宣告變數 bar 儲存在[[Scopes]][0]: Script這個變數物件的屬性中,而[[Scopes]][1]: Global就是我們常說的全域性物件。


(4)變數提升與暫存死區

var 宣告變數存在變數提升,如何理解變數提升呢?

要解釋清楚這個,就要涉及到執行上下文變數物件

在 JavaScript 程式碼執行時,解釋執行全域性程式碼、呼叫函式或使用 eval 函式執行一個字串表示式都會建立並進入一個新的執行環境,而這個執行環境被稱之為執行上下文。因此執行上下文有三類:全域性執行上下文、函式執行上下文、eval 函式執行上下文。

執行上下文可以理解為一個抽象的物件,如下圖:

執行上下文抽象物件

Variable object:變數物件,用於儲存被定義在執行上下文中的變數 (variables) 和函式宣告 (function declarations) 。

Scope chain:作用域鏈,是一個物件列表 (list of objects) ,用以檢索上下文程式碼中出現的識別符號 (identifiers) 。

thisValue:this 指標,是一個與執行上下文相關的特殊物件,也被稱之為上下文物件。


一個執行上下文的生命週期可以分為三個階段:建立、執行、釋放。如下圖:

執行上下文的生命週期

而所有使用 var 宣告的變數都會在執行上下文的建立階段時作為變數物件的屬性被建立並初始化,這樣才能保證在執行階段能通過識別符號在變數物件裡找到對應變數進行賦值操作等。

而用 var 宣告的變數構建變數物件時進行的操作如下:

  • 由名稱和對應值(undefined)組成一個變數物件的屬性被建立(建立並初始化)
  • 如果變數名稱跟已經宣告的形式引數或函式相同,則變數宣告不會干擾已經存在的這類屬性。

上述過程就是我們所謂的“變數提升”,這也就能解釋為什麼變數可以在宣告之前使用,因為使用是在執行階段,而在此之前的建立階段就已經將宣告的變數新增到了變數物件中,所以執行階段通過識別符號可以在變數物件中查詢到,也就不會報錯。

示例程式碼:

console.log(a) // undefined

var a = 1;

console.log(a) // 1

let 宣告變數存在暫存死區,如何理解暫存死區呢?

其實 let 也存在與 var 類似的“變數提升”過程,但與 var 不同的是其在執行上下文的建立階段,只會建立變數而不會被初始化(undefined),並且 ES6 規定了其初始化過程是在執行上下文的執行階段(即直到它們的定義被執行時才初始化),使用未被初始化的變數將會報錯。

let and const declarations define variables that are scoped to the running execution context’s LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.

在變數初始化前訪問該變數會導致 ReferenceError,因此從進入作用域建立變數,到變數開始可被訪問的一段時間(過程),就稱為暫存死區(Temporal Dead Zone)。

示例程式碼 1:

console.log(bar); // undefined
console.log(foo); // ReferenceError: foo is not defined

var bar = 1;
let foo = 2;

示例程式碼 2:

var foo = 33;
{
  let foo = (foo + 55); // ReferenceError: foo is not defined
}

注:首先,需要分清變數的建立、初始化、賦值是三個不同的過程。另外,從 ES5 開始用詞法環境(Lexical Environment)替代了 ES3 中的變數物件(Variable object)來管理靜態作用域,但作用是相同的。為了方便理解,上述講解中仍保留使用變數物件來進行描述。


小結

  1. var 宣告的變數在執行上下文建立階段就會被「建立」和「初始化」,因此對於執行階段來說,可以在宣告之前使用。

  2. let 宣告的變數在執行上下文建立階段只會被「建立」而不會被「初始化」,因此對於執行階段來說,如果在其定義執行前使用,相當於使用了未被初始化的變數,會報錯。


3.let 與 const 異同

const 與 let 很類似,都具有上面提到的 let 的特性,唯一區別就在於 const 宣告的是一個只讀變數,宣告之後不允許改變其值。因此,const 一旦宣告必須初始化,否則會報錯。

示例程式碼:

let a;
const b = "constant"

a = "variable"
b = 'change' // TypeError: Assignment to constant variable

如何理解宣告之後不允許改變其值?

其實 const 其實保證的不是變數的值不變,而是保證變數指向的記憶體地址所儲存的資料不允許改動(即棧記憶體在的值和地址)。

JavaScript 的資料型別分為兩類:原始值型別和物件(Object型別)。

對於原始值型別(undefined、null、true/false、number、string),值就儲存在變數指向的那個記憶體地址(在棧中),因此 const 宣告的原始值型別變數等同於常量。

對於物件型別(object,array,function等),變數指向的記憶體地址其實是儲存了一個指向實際資料的指標,所以 const 只能保證指標是不可修改的,至於指標指向的資料結構是無法保證其不能被修改的(在堆中)。

示例程式碼:

const obj = {
  value: 1
}

obj.value = 2

console.log(obj) // { value: 2 }

obj = {} // TypeError: Assignment to constant variable

4.參考

var - JavaScript | MDN

let - JavaScript - MDN - Mozilla

const - JavaScript - MDN - Mozilla

深入理解JavaScript系列(12):變數物件(Variable Object)

ES6 let 與 const

詳解ES6暫存死區TDZ

嗨,你知道 let 和 const 嗎?

相關文章