我用了兩個(gè)月的時(shí)間才理解 let 2018-09-03

當(dāng)然不是說用完整的兩個(gè)月時(shí)間來理解 let,而是零零碎碎地理解,同時(shí)我還要想著怎么寫出一篇文章把這個(gè)事情說清楚。

在 let 剛出來的時(shí)候,我就「以為」我理解了 let。然鵝在過去的兩個(gè)月里,我對(duì) let 的理解發(fā)生了一波三折的變化。

我寫這篇文章,是希望我的學(xué)習(xí)過程,能對(duì)你自學(xué)有幫助。

初識(shí) let

跟很多人一樣,我第一次了解 let 的特性是從 MDN 的文檔:

我得到的信息有這么幾條:

  • let 聲明的變量的作用域是塊級(jí)的;
  • let 不能重復(fù)聲明已存在的變量;
  • let 有暫時(shí)死區(qū),不會(huì)被提升。

大部分人應(yīng)該都是這么認(rèn)為的,我也是這么理解的。

這個(gè)理解「沒有問題」,但是不夠「全面和深刻」。

第一次質(zhì)疑

我第一次質(zhì)疑我的理解是在遇到 for 循環(huán)的時(shí)候,代碼如下。

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

大家都知道依次點(diǎn)擊 li 會(huì)打印出 5 個(gè) 5。如果把 var i 改成 let i,就會(huì)分別打印出 0、1、2、3、4

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

然而,用我之前的知識(shí)來理解這個(gè)代碼是不能自圓其說的。因?yàn)榇a中依然只聲明了一個(gè) i,在 for 循環(huán)結(jié)束后,i 的值還是會(huì)變成 5 才對(duì)。

這說明我的理解有誤。

于是我去看 MDN 的例子,發(fā)現(xiàn)雞賊的 MDN 巧妙地避開了這個(gè)問題,它的例子是這樣的:

你看 MDN 的例子,在每次循環(huán)的時(shí)候用 let j 保留的 i 的值,所以在 i 變化的時(shí)候,j 并不會(huì)變化。而console.log 的是 j,所以不會(huì)出現(xiàn) 5 個(gè) 5。

為什么 MDN 要故意聲明一個(gè) j 呢,為什么不直接用 i 呢?

我猜測(cè) MDN 為了簡(jiǎn)化知識(shí),隱瞞了什么。

于是我去看了看 ES 文檔,其中的 13.7.4.7 章節(jié) 清楚地說明了個(gè)中緣由,但是由于說得太清楚了,很多人都看不下去,不信你可以試試。

我說一下我的理解:

  1. for( let i = 0; i< 5; i++) 這句話的圓括號(hào)之間,有一個(gè)隱藏的作用域
  2. for( let i = 0; i< 5; i++) { 循環(huán)體 } 在每次執(zhí)行循環(huán)體之前,JS 引擎會(huì)把 i 在循環(huán)體的上下文中重新聲明及初始化一次。
  3. 其他細(xì)節(jié)就不說了,太細(xì)碎了

也就是說上面的代碼段2可以近似近似近似地理解為

// 代碼段3
var liList = document.querySelectorAll('li') // 共5個(gè)li
for( let i=0; i<liList.length; i++){
  let i = 隱藏作用域中的i // 看這里看這里看這里
  liList[i].onclick = function(){
    console.log(i)
  }
}

那樣的話,5 次循環(huán),就會(huì)有 5 個(gè)不同的 i,console.log 出來的 i 當(dāng)然也是不同的值。

再加上隱藏作用域里的 i,一共有 6 個(gè) i。

這就是 MDN 加那句 let j = i 的原因:方便新人理解。

總得來說就是 let/const 在與 for 一起用時(shí),會(huì)有一個(gè) perIterationBindings 的概念(一種語法糖)。

從此之后,我就開始懷疑我對(duì) let 的所有理解了。

第二次質(zhì)疑

我在 StackOverflow 上閑逛的時(shí)候,無意中發(fā)現(xiàn)了一個(gè)是關(guān)于「let 到底有沒有提升」的問題:

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

其中一個(gè)高票回答認(rèn)為 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 翻了翻,發(fā)現(xiàn)兩處疑點(diǎn):

  1. MDN 關(guān)于 let 是否存在提升的章節(jié),被編輯了兩次,第一次說存在提升,第二次說不存在提升(參考 2017 年 3 月 10 號(hào)的變更記錄)。也就是說 MDN 的維護(hù)者都在這個(gè)問題上產(chǎn)生過分歧,更何況我們了。
  2. ES 文檔里出現(xiàn)了「var/let hoisting」字樣。

鑒于此,我認(rèn)為應(yīng)該尊重 ES 文檔,認(rèn)為 let 確實(shí)存在提升。只不過由于暫時(shí)死區(qū)的限制,你不能在 let x 之前使用 let(詳見 let 聲明會(huì)提升(hoist)嗎?)。

故事并沒有結(jié)束

當(dāng)一個(gè)疑問一直存在你腦中時(shí),你會(huì)在潛意識(shí)中不停地對(duì)它進(jìn)行消化和思考。

上面說到我認(rèn)為 let 存在提升的主要原因是 ES 文檔中出現(xiàn)了「var/let hoisting」字樣。

但是我在咨詢 TC39 的成員 <u style="text-decoration: none; border-bottom: 1px dashed gray;">Rick Waldron</u> 時(shí),他是這么說的:

You're misunderstanding the intention of that (non-normative) Note. let and const do not hoist as var and function decls do.

而且還細(xì)心地專門寫了一個(gè) gist 來詳細(xì)解釋(twitter 有 140 字限制)。

既然 TC39 的 representative 都說了 let hoisting 不是一個(gè)正式詞匯,那我就應(yīng)該重新思考一下什么是「提升」了。

于是就有了這篇文章,我想要說一下我對(duì)「提升」的理解。你看完之后,就知道 let 到底有沒有「提升」。

首先明確一點(diǎn):提升不是一個(gè)技術(shù)名詞。

要搞清楚提升的本質(zhì),需要理解 JS 變量的「創(chuàng)建create、初始化initialize 和賦值assign」

有的地方把創(chuàng)建說成是聲明(declare),為了將這個(gè)概念與變量聲明區(qū)別開,我故意不使用聲明這個(gè)字眼。

有的地方把初始化叫做綁定(binding),但我感覺這個(gè)詞不如初始化形象。

我們來看看 var 聲明的「創(chuàng)建、初始化和賦值」過程

假設(shè)有如下代碼:

function fn(){
  var x = 1
  var y = 2
}
fn()

在執(zhí)行 fn 時(shí),會(huì)有以下過程(不完全):

  1. 進(jìn)入 fn,為 fn 創(chuàng)建一個(gè)環(huán)境。
  2. 找到 fn 中所有用 var 聲明的變量,在這個(gè)環(huán)境中「創(chuàng)建」這些變量(即 x 和 y)。
  3. 將這些變量「初始化」為 undefined。
  4. 開始執(zhí)行代碼
  5. x = 1 將 x 變量「賦值」為 1
  6. y = 2 將 y 變量「賦值」為 2

也就是說 var 聲明會(huì)在代碼執(zhí)行之前就將「創(chuàng)建變量,并將其初始化為 undefined」。

這就解釋了為什么在 var x = 1 之前 console.log(x) 會(huì)得到 undefined。

接下來來看 function 聲明的「創(chuàng)建、初始化和賦值」過程

假設(shè)代碼如下:

fn2()

function fn2(){
  console.log(2)
}

JS 引擎會(huì)有一下過程:

  1. 找到所有用 function 聲明的變量,在環(huán)境中「創(chuàng)建」這些變量。
  2. 將這些變量「初始化」并「賦值」為 function(){ console.log(2) }。
  3. 開始執(zhí)行代碼 fn2()

也就是說 function 聲明會(huì)在代碼執(zhí)行之前就「創(chuàng)建、初始化并賦值」。

接下來看 let 聲明的「創(chuàng)建、初始化和賦值」過程

假設(shè)代碼如下:

{
  let x = 1
  x = 2
}

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

  1. 找到所有用 let 聲明的變量,在環(huán)境中「創(chuàng)建」這些變量
  2. 開始執(zhí)行代碼(注意現(xiàn)在還沒有初始化)
  3. 執(zhí)行 x = 1,將 x 「初始化」為 1(這并不是一次賦值,如果代碼是 let x,就將 x 初始化為 undefined)
  4. 執(zhí)行 x = 2,對(duì) x 進(jìn)行「賦值」

這就解釋了為什么在 let x 之前使用 x 會(huì)報(bào)錯(cuò):

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

原因有兩個(gè)

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

看到這里,你應(yīng)該明白了 let 到底有沒有提升:

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

最后看 const,其實(shí) const 和 let 只有一個(gè)區(qū)別,那就是 const 只有「創(chuàng)建」和「初始化」,沒有「賦值」過程。

這四種聲明,用下圖就可以快速理解:


完了嗎?

故事依然沒有結(jié)束,這周我在知乎上問了一個(gè)問題:如何理解 let x = x 報(bào)錯(cuò)之后,再次 let x 依然會(huì)報(bào)錯(cuò)?

這個(gè)問題說明:如果 let x 的初始化過程失敗了,那么

  1. x 變量就將永遠(yuǎn)處于 created 狀態(tài)。
  2. 你無法再次對(duì) x 進(jìn)行初始化(初始化只有一次機(jī)會(huì),而那次機(jī)會(huì)你失敗了)。
  3. 由于 x 無法被初始化,所以 x 永遠(yuǎn)處在暫時(shí)死區(qū)(也就是盜夢(mèng)空間里的 limbo)!
  4. 有人會(huì)覺得 JS 坑,怎么能出現(xiàn)這種情況;其實(shí)問題不大,因?yàn)榇藭r(shí)代碼已經(jīng)報(bào)錯(cuò)了,后面的代碼想執(zhí)行也沒機(jī)會(huì)。

細(xì)節(jié)參見另一篇文章:JS變量封禁大法:薛定諤的X

以上,就是一個(gè) let 引發(fā)的思考。

重要參考:JavaScript variables lifecycle: why let is not hoisted

重要參考:鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容