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

目錄

  • 序言
  • 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 嗎?