嗨,你知道 let 和 const 嗎?

前言

若是有人問你:知道 let 和 const 嗎 答案是確定的,還能回答的頭頭是道:javascript

  • let 和 const 不就是 es6 新增的聲明變量的關鍵字嘛
  • let 是用來聲明變量的,const 是用來聲明常量的
  • 與 var 不一樣,let、const 不存在變量提高
  • 暫時性死區
  • 不綁定頂級對象

再問:前端

  • let 和 const 真的不存在變量提高嗎? 爲何不存在
  • 暫時性死區又是如何造成的?
  • let、const 聲明的全局變量不在 全局對象中,那它存在於哪裏呢?

前言 2

我是一個前端菜鳥,常用 let、const,我知道 let (let 和 const 的基本特性一致,後面就不帶 const 了) 不存在變量提高,變量操做必須在聲明以後,不然就會報錯;還知道 let 聲明的變量存在於塊級做用域中,塊外沒法訪問塊內使用 let 聲明的變量;我還知道 let 聲明的變量不會被掛載到全局對象中(剛知道);java

好久之前,我知道了執行上下文中的變量對象這個東西,理解了變量對象,繼而理解了變量提高的過程,後來心中一直有一個疑惑,使用 var 聲明的變量會被建立在 變量對象中,也就是所謂的變量提高,let 又爲何不會變量提高呢;es6

1. var 的變量提高規則

複習變量提高的概念,其中涉及到 JS 的 執行上下文、變量對象 等知識。瀏覽器

var a = "global";
function bar(a) {
  console.log(temp); // undefined
  var temp = "local";
}

bar();
複製代碼

咱們都知道在 js 中,代碼執行時會把當前做用域中的全部的使用 var 聲明的變量以及 function 聲明的函數提高到做用域的頂部(變量提高聲明,不提高賦值;函數提高函數體,不提高調用)。因此咱們在 var 聲明變量以前能夠訪問 temp,結果是 undefined緩存

讓咱們從 js 代碼的運行角度去看這個過程,以下:函數

<1> 執行流進入全局執行環境post

<2> 建立全局執行環境的變量對象, 並將變量 a 的聲明、函數 bar 的聲明添加到變量對象中ui

<3> 進入全局代碼執行階段lua

<4> 對變量 a 賦值爲 'global'

<5> 遇到 函數 bar 的調用

<6> 進入函數 bar 內部,建立 bar 的執行環境,壓入執行環境棧

<7> 一樣的,建立函數 bar 執行環境的活動對象,將變量 temp 在 活動對象內建立,key 爲 temp,初始化值爲 undefined

<8> 進入代碼執行階段,遇到 打印輸出 a,此時 a 存在於活動對象中,值爲 undefined

<9> 繼續執行,爲 temp 賦值爲 'local'

<10> bar 函數內代碼執行完畢,執行環境棧談出 bar 執行環境,執行流回到 global 執行環境中繼續執行...

小結

js 在代碼預編譯階段,會建立一個變量對象,變量對象中有一個屬性,屬性名稱爲 temp,屬性值爲 undefined,因此在變量聲明以前打印 temp,值爲 undefined;這也就是 var 的變量提高規則

2. let 的提高規則

死緩區? 暫存死區? TDZ (Temporal dead zone)?

若是你能理解 var 的提高規則,那麼理解 let 的提高將會變得很輕鬆。 在 MDN 關於 let 的文檔中,有這麼一句話:

The other difference between var and let is that the latter is initialized to value only when parser evaluates it (see below).

ps: 個人英語極差,MDN 的翻譯是這樣的:

var 和 let 的不一樣之處在於後者是在編譯時才初始化(見下面)

我使用 有道翻譯 是醬紫的:

var 和 let 之間的另外一個區別是,後者只有在解析器對其求值時才初始化爲 value(參見下面的內容)。

我預感到 MDN 的中文翻譯並不許確(由於 var 是在編譯時初始化的,可見翻譯有問題),再結合 有道的硬核翻譯,我將其理解爲:

var 和 let 的不一樣之處在於後者是在運行時才初始化的

在 MDN 上點擊 (見下面) 看到了 暫存死區的概念:

與經過 var 聲明的有初始化值 undefined 的變量不一樣,經過 let 聲明的變量直到它們的定義被執行時才初始化。在變量初始化前訪問該變量會致使 ReferenceError。該變量處在一個自塊頂部到初始化處理的「暫存死區」中。

結合例子:

例 1.

function foo() {
  console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
  let a = 123;
}
foo();
複製代碼

典型的 let 聲明變量的例子:

與 var 不一樣的是,咱們沒法在 let 聲明以前獲取該變量;

不然,報錯:Cannot access 'a' before initialization;

報錯信息給到咱們,硬核翻譯爲:沒法在初始化以前訪問'a';

從 「沒法在初始化以前訪問'a'」 是否可以看出寫端倪呢?

例 2.

let a = 3;
let b;
(function() {
  console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
  console.log(b);
  let a = (b = 3);
  console.log(a);
  console.log(b);
})();
console.log(a);
console.log(b);
複製代碼

結果爲:報錯 - Uncaught ReferenceError: Cannot access 'a' before initialization;

與 例 1 報的一樣的錯誤;

咱們試想下:

在匿名函數內,第一個訪問的 a,爲何不能獲取到外部環境的 a = 3;而是在這裏就早早的報錯了呢?

估計不少人就會說:這不就是由於 let 的暫時性死區特性嘛;

在 阮大神的 《es6 入門》 中,對於暫時性死區這樣寫到:

只要塊級做用域內存在 let 命令,它所聲明的變量就「綁定」(binding)這個區域,再也不受外部的影響。

以及後面的介紹,在我看來只是聲明式的告訴了我:在做用域內,不管代碼前後順序,只要在代碼中出現了 let a,那麼 a 變量就 「綁定」 了這個做用域,有沒有疑問? 爲何我聲明變量的代碼明明還在下面,上面的代碼就被綁定了呢,js 不是順序執行的嗎?

OK,走到了如今,結合 var 的提高規則,是否是能夠將 let 理解爲:

  • 使用 let 聲明的變量實際上也存在提高,可是與 var 的提高規則不一樣:
  • var 聲明的變量是在代碼預編譯階段 被建立在了 執行環境的變量對象中,而且將其初始化,初始化值爲 undefined;
  • 而 let 聲明的變量,在執行環境預編譯階段,被提高到了一個叫作 「暫存死區」 的地方,而且沒有對其進行初始化
  • 因此,不管使用 let 聲明的變量,聲明在代碼的任何位置,這個變量一開始就被 「綁定」 在了當前執行環境中;
  • 在進入執行階段時,只有在 let 聲明被執行後,纔對這個變量進行了初始化;
  • 因此,在執行到 let 聲明以前,是沒法訪問這個被 「綁定」 的變量的。

進一步驗證

let a = 3;
let b;
(function() {
  let a;
  console.log(a); // undefined
  console.log(b); // undefined
  b = 3;
  a = b;
  console.log(a); // 3
  console.log(b); // 3
})();
console.log(a); // 3
console.log(b); // 3
複製代碼

代碼順利執行

咱們將變量 a 的聲明提高到了函數的頂部,在進入函數執行階段,第一句執行的就是 let a; 聲明瞭 a 變量,並將其值初始化爲 undefined

因此在下面打印 a 爲 undefined

後續代碼就不說了...

小結

  1. 其實 let 和 const 也是存在變量提高的,只不過和 var 的提高規則不一樣,let 是將變量提高到了一個叫作 「暫存死區」 的地方,在提高時並無對其進行初始化,若是去訪問 「暫存死區」 中的變量,就會報錯;
  2. 因爲 let 的變量提高是在 「預編譯」 階段完成的,因此在進入執行環境後,不管聲明代碼在何處,這個被聲明的變量就被綁定了,在聲明以前訪問這個變量,就會報錯,也就會造成 代碼的 「暫時性死區」
  3. 在執行環境的執行階段,在當執行到 let 聲明時,那個變量纔會 從 「暫存死區」 中移除,並對齊初始化爲 undefined,因此 使用 let 聲明的變量只能在其聲明後訪問。
  4. 其實在個人理解中,「暫存死區」「暫時性死區」 並非一個概念,「暫存死區」 亦可稱爲 「死緩區」 (死亡緩存區域? 隨便怎麼叫了),「暫存死區」 是一個保存 let 聲明的變量的地方,正是由於有這個區域的存在,才使得 let 聲明的變量可以保證不能再聲明前訪問變量,繼而也就造成了 「暫時性死區」

3. 全局環境下的 let

在 《es6 入門》 中如是寫到:

頂層對象,在瀏覽器環境指的是 window 對象,在 Node 指的是 global 對象。ES5 之中,頂層對象的屬性與全局變量是等價的。

let 命令、const 命令、class 命令聲明的全局變量,不屬於頂層對象的屬性。

在 es5 時期,全局聲明的變量是被掛載在頂級對象下的,在 es6 時期,使用 let 聲明的全局變量,並不存在於頂級對象下

那麼,使用 let 聲明的變量存在於哪裏呢?

var a = 1;
let b = 2;

console.log(window.a); // 1
console.log(window.b); // undefined
複製代碼

下列涉及到做用域、做用域鏈生成規則等知識, 若是不理解做用鏈的生成規則可能會對下面產生疑惑。

友情連接:瞭解做用域 | 深刻做用域鏈

let str = "global Str";
function bar() {
  let str = "local Str";
}

console.dir(bar);
複製代碼

輸出結果:

做用域鏈

上圖中的 圈紅部分是函數的做用域鏈(在 谷歌瀏覽器能夠看到如上的打印輸出,在 火狐 ie 下均沒有)

能夠看到,在做用域鏈中存在兩項:

  • [[Scopes]][0]: Script 和頂級對象平行的一個做用域,能夠看到裏面貌似有熟悉的東西
  • [[Scopes]][1]: Global 也就是頂級做用域的變量對象,在瀏覽器中就是 window

小結

  • let 定義的全局變量並不存在於 頂級對象中,而是存在於和頂級對象平行的一個全局做用域中

  • 亦或者能夠說 let 和 const 定義的變量是存在於做用鏈的頂端的,根據做用域鏈的訪問規則,能夠訪問到全局變量

  • 至於這個 做用域鏈頂端的 Script 究竟是什麼,我也說不清楚,我目前沒有仔細去找解釋這個 [[Scopes]][0]: Script 的文檔,大體的找了下,並無找到,還望有了解的大佬解釋下這個 做用域鏈 頂端的 Script 究竟是個什麼東西。

乾貨總結

  • let 和 const 也存在變量提高,預編譯階段提高,因此能綁定整個做用域
  • let 和 const 將變量提高到了一個稱爲 「死緩區」 的地方,嘗試訪問 「死緩區」 內容將會報錯,因此造成 「暫時性死區」
  • let 和 const 在提高變量時不會對其初始化操做
  • let 和 cosnt 聲明的全局變量在 做用域鏈的頂端,一個叫 Script 的做用域裏,根據做用域鏈規則,能夠訪問到其定義的全局變量

本文完,歡迎各位看官老爺批評

相關文章
相關標籤/搜索