若是有人問你:知道 let 和 const 嗎 答案是確定的,還能回答的頭頭是道:javascript
再問:前端
我是一個前端菜鳥,常用 let、const,我知道 let (let 和 const 的基本特性一致,後面就不帶 const 了) 不存在變量提高,變量操做必須在聲明以後,不然就會報錯;還知道 let 聲明的變量存在於塊級做用域中,塊外沒法訪問塊內使用 let 聲明的變量;我還知道 let 聲明的變量不會被掛載到全局對象中(剛知道);java
好久之前,我知道了執行上下文中的變量對象這個東西,理解了變量對象,繼而理解了變量提高的過程,後來心中一直有一個疑惑,使用 var 聲明的變量會被建立在 變量對象中,也就是所謂的變量提高,let 又爲何不會變量提高呢;es6
複習變量提高的概念,其中涉及到 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 的變量提高規則
若是你能理解 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。該變量處在一個自塊頂部到初始化處理的「暫存死區」中。
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'」 是否可以看出寫端倪呢?
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
後續代碼就不說了...
- 其實 let 和 const 也是存在變量提高的,只不過和 var 的提高規則不一樣,let 是將變量提高到了一個叫作 「暫存死區」 的地方,在提高時並無對其進行初始化,若是去訪問 「暫存死區」 中的變量,就會報錯;
- 因爲 let 的變量提高是在 「預編譯」 階段完成的,因此在進入執行環境後,不管聲明代碼在何處,這個被聲明的變量就被綁定了,在聲明以前訪問這個變量,就會報錯,也就會造成 代碼的 「暫時性死區」
- 在執行環境的執行階段,在當執行到 let 聲明時,那個變量纔會 從 「暫存死區」 中移除,並對齊初始化爲 undefined,因此 使用 let 聲明的變量只能在其聲明後訪問。
- 其實在個人理解中,「暫存死區」 和 「暫時性死區」 並非一個概念,「暫存死區」 亦可稱爲 「死緩區」 (死亡緩存區域? 隨便怎麼叫了),「暫存死區」 是一個保存 let 聲明的變量的地方,正是由於有這個區域的存在,才使得 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 究竟是個什麼東西。