【前端100問】Q83:var、let 和 const 區別的實現原理是什麼

寫在前面

此係列來源於開源項目:前端 100 問:能搞懂 80%的請把簡歷給我
爲了備戰 2021 春招
每天一題,督促自己
從多方面多角度總結答案,豐富知識
var、let 和 const 區別的實現原理是什麼
簡書整合地址:前端 100 問

正文回答

第一次質疑

我第一次質疑我的理解是在遇到 for 循環的時候,代碼如下。

// 代碼段1
var liList = document.querySelectorAll("li"); // 共5個li
for (var i = 0; i < liList.length; i++) {
  liList[i].onclick = function () {
    console.log(i);
  };
}

大家都知道依次點擊 li 會打印出 5 個 5。如果把 var i 改成 let i,就會分別打印出 0、1、2、3、4:

// 代碼段2
var liList = document.querySelectorAll("li"); // 共5個li
for (let i = 0; i < liList.length; i++) {
  liList[i].onclick = function () {
    console.log(i);
  };
}

然而,用我之前的知識來理解這個代碼是不能自圓其說的。因爲代碼中依然只聲明瞭一個 i,在 for 循環結束後,i 的值還是會變成 5 纔對。

第二次質疑

我在 StackOverflow 上閒逛的時候,無意中發現了一個是關於「let 到底有沒有提升」的問題:

Are variables declared with let or const not hoisted in ES6?

其中一個高票回答認爲 JS 中所有的聲明(var/let/const/function/class),都存在提升,理由是如下代碼:

x = "global";
// function scope:
(function() {
    x; // not "global"
    var/let/… x;
}());
// block scope (not for `var`s):
{
    x; // not "global"
    let/const/… x;
}

我覺得他說得挺有道理的。於是我又去 MDN 和 ECMAScript 翻了翻,發現兩處疑點:

  1. MDN 關於 let 是否存在提升的章節,被編輯了兩次,第一次說存在提升,第二次說不存在提升(參考 2017 年 3 月 10 號的變更記錄)。也就是說 MDN 的維護者都在這個問題上產生過分歧,更何況我們了。
  2. ES 文檔裏出現了「var/let hoisting」字樣。
創建、初始化和賦值
我們來看看 var 聲明的「創建、初始化和賦值」過程
function fn() {
  var x = 1;
  var y = 2;
}
fn();

在執行 fn 時,會有以下過程(不完全):

  1. 進入 fn,爲 fn 創建一個環境。
  2. 找到 fn 中所有用 var 聲明的變量,在這個環境中「創建」這些變量(即 x 和 y)。
  3. 將這些變量「初始化」爲 undefined。
  4. 開始執行代碼
  5. x = 1 將 x 變量「賦值」爲 1
  6. y = 2 將 y 變量「賦值」爲 2

也就是說 var 聲明會在代碼執行之前就將「創建變量,並將其初始化爲 undefined」。

這就解釋了爲什麼在 var x = 1 之前 console.log(x) 會得到 undefined。

接下來來看 function 聲明的「創建、初始化和賦值」過程
fn2();
function fn2() {
  console.log(2);
}

JS 引擎會有以下過程:

  1. 找到所有用 function 聲明的變量,在環境中「創建」這些變量。
  2. 將這些變量「初始化」並「賦值」爲 function(){ console.log(2) }。
  3. 開始執行代碼 fn2()

也就是說 function 聲明會在代碼執行之前就「創建、初始化並賦值」。

接下來看 let 聲明的「創建、初始化和賦值」過程
{
  let x = 1;
  x = 2;
}

我們只看 {} 裏面的過程:

  1. 找到所有用 let 聲明的變量,在環境中「創建」這些變量
  2. 開始執行代碼(注意現在還沒有初始化)
  3. 執行 x = 1,將 x 「初始化」爲 1(這並不是一次賦值,如果代碼是 let x,就將 x 初始化爲 undefined)
  4. 執行 x = 2,對 x 進行「賦值」

這就解釋了爲什麼在 let x 之前使用 x 會報錯:

let x = "global";
{
  console.log(x); // Uncaught ReferenceError: x is not defined
  let x = 1;
}

原因有兩個

  1. console.log(x) 中的 x 指的是下面的 x,而不是全局的 x
  2. 執行 log 時 x 還沒「初始化」,所以不能使用(也就是所謂的暫時死區)

看到這裏,你應該明白了 let 到底有沒有提升:

  1. let 的「創建」過程被提升了,但是初始化沒有提升。
  2. var 的「創建」和「初始化」都被提升了。
  3. function 的「創建」「初始化」和「賦值」都被提升了。

最後看 const,其實 const 和 let 只有一個區別,那就是 const 只有「創建」和「初始化」,沒有「賦值」過程。

所謂暫時死區,就是不能在初始化之前,使用變量

原文:我用了兩個月的時間才理解 let

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章