說到ES6的 變數聲明,我估計很多人會想起下麵幾個主要的特點: 沒有變數聲明提升 擁有塊級作用域 暫時死區 不能重覆聲明 很多教程和總結基本都說到了這幾點(說實話大部分文章都大同小異,摘錄的居多),習慣性我還是去看了MDN上的文檔,立馬發現一個問題: In ECMAScript 2015, let ...
說到ES6的let
變數聲明,我估計很多人會想起下麵幾個主要的特點:
- 沒有變數聲明提升
- 擁有塊級作用域
- 暫時死區
- 不能重覆聲明
很多教程和總結基本都說到了這幾點(說實話大部分文章都大同小異,摘錄的居多),習慣性我還是去看了MDN上的文檔,立馬發現一個問題:
In ECMAScript 2015, let will hoist the variable to the top of the block. However, referencing the variable in the block before the variable declaration results in a ReferenceError. The variable is in a "temporal dead zone" from the start of the block until the declaration is processed.
ECMAScript 2015(即ES6),let會提升變數到代碼塊頂部。然而,在變數聲明前引用變數會導致ReferenceError錯誤。在代碼塊開始到變數聲明之間變數處於暫時死區(temporal dead zone)。
不得了,看來let是有變數聲明提升的啊,這個發現引起了我的興趣。我立馬去找了一些相關的資料查看,在查看的過程中,我也慢慢瞭解了其他一些隱含的容易誤解的知識點,下麵羅列一些相關資料,方便讓有同樣興趣瞭解的童鞋去查閱:
- 關於對let變數聲明的:
ES6 In Depth: let and const
Are variables declared with let or const not hoisted in ES6? - js變數作用域概念,比較基礎
What is the scope of variables in JavaScript? - 這一篇文章也很基礎,從作用域,上下文,this,執行上下文,閉包,立即執行函數,等等都講了一遍,稍微提到了詞法作用域(lexical scope)即靜態作用域。
Understanding Scope in JavaScript - 這裡通過例子解釋詞法作用域(lexical scope),很容易理解
What is lexical scope? - 對於for迴圈中let的表現說明
Why is let slower than var in a for loop in nodejs? - 這個很多東西都講到了,也涉及let的作用域到for迴圈,不過文章好長,我只看了相關到部分。
You Don't Know JS: Scope & Closures
不願意去翻閱資料的就看我下麵的個人總結吧。
變數聲明提升
關於變數聲明提升,有幾個重點:
- 所有的變數聲明( var, let, const, function, function*, class)都存在變數聲明提升,我們這裡只談論let變數
- let被提升到了塊級作用域的頂部,表現(或者說換種說法)就是每個let定義的變數都綁定到了當前的塊級作用域內。通俗地講,因為塊級作用域在頂部就為每個let定義的變數留好了位置,所以只要在let變數聲明前引用了這個變數名,塊級作用域都會發現並拋出錯誤
- var的變數聲明提升會將變數初始化為undefined,let沒有初始化,所以有暫時死區的概念。其實從表現上來講,說let是沒有變數聲明提升也有一定道理,因為變數沒有在頂部初始化,所以也不能說變數已經聲明過了,反而用綁定到了當前的塊級作用域內這種說法更令人信服
在我的思路大概清晰寫這篇總結的時候,我又偶然在一篇講變數聲明提升的博文上看到一段MDN原文的引用:
In ECMAScript 6, let does not hoist the variable to the top of the block. If you reference a variable in a block before the let declaration for that variable is encountered, this results in a ReferenceError, because the variable is in a "temporal dead zone" from the start of the block until the declaration is processed.
納尼!居然和我現在看到的MDN文檔不一致......博文的日期是2015-06-11,看來這個概念也在改變,與時俱進啊。既然如此,我覺得也沒有必要深究了,因為不管概念怎麼變,只要能夠知道let在塊級作用域的正確表現就可以了,理論還是要為實踐服務。
let在for迴圈中的表現
for的運行機制
說到for迴圈,先說明下for的運行機制,比如說for(var i=0;i<10;i++){...}即先初始化迴圈變數(var i=0),這一句只運行一次,然後進行比較(i<10),然後運行函數體{...},函數體運行結束後,如果沒有break等跳出,再運行自增表達式(i++),然後進行比較判斷(i<10)是否進入執行體。下麵是引用別人的一個回答How are for loops executed in javascript?,將這個過程描述得很清晰:
// for(initialise; condition; finishediteration) { iteration }
var initialise = function () { console.log("initialising"); i=0; }
var condition = function () { console.log("conditioning"); return i<5; }
var finishediteration = function () { console.log("finished an iteration"); i++; }
var doingiteration = function () { console.log("doing iteration when `i` is equal", i); }
for (initialise(); condition(); finishediteration()) {
doingiteration();
}
initialising
conditioning
doing iteration when `i` is equal 0
finished an iteration
conditioning
doing iteration when `i` is equal 1
finished an iteration
conditioning
doing iteration when `i` is equal 2
finished an iteration
conditioning
doing iteration when `i` is equal 3
finished an iteration
conditioning
doing iteration when `i` is equal 4
finished an iteration
conditioning
for迴圈中的let
之所以要單獨講for迴圈中的let,是因為看到了阮老師ES6入門中講let的那一章的一個例子:
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
對這個例子原文中是這樣的解釋的:
上面代碼中,變數i是let聲明的,當前的i只在本輪迴圈有效,所以每一次迴圈的i其實都是一個新的變數,所以最後輸出的是6。你可能會問,如果每一輪迴圈的變數i都是重新聲明的,那它怎麼知道上一輪迴圈的值,從而計算出本輪迴圈的值?這是因為 JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算。
JavaScript 引擎內部會記住上一輪迴圈的值
這句解釋我覺得作為程式猿估計怎麼都無法認可吧?記住這個詞說得太模糊了,其中固然有某種機制或規範。而且每一輪迴圈的變數i都是重新聲明
,那麼下麵的例子就難以解釋:
for (let i = 0; i < 5; i++){
i++;
console.log(i)
}
// 1
// 3
// 5
如果迴圈函數體內的i
每次都是重新聲明的,那麼函數體內即子作用域內改變i
的值,為什麼能夠改變外層定義的i
變數?
再來看文中提的另外一個例子:
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
這個例子原文的解釋是:
迴圈語句部分是一個父作用域,而迴圈體內部是一個單獨的子作用域。
如果按照上面的邏輯每個子作用域內的i
都重新聲明,那麼在同一個子作用域內為什麼能夠二次聲明?
很明顯,i
並沒有重新聲明。看來我們有必要藉助其他文檔來幫助理解。
MDN上的文檔,提到for迴圈中,每進入一次花括弧就生成了一個塊級域,即每個迴圈進入函數體的
i
都綁定到了不同的塊級域中,由於是不同的塊級作用域,所以每次進入迴圈體函數的i
值都相互獨立,不是同一個作用域下的值。ES6 In Depth: let and const文章中是這樣解釋的:
each closure will capture a different copy of the loop variable, rather than all closures capturing the same loop variable.
每一個閉包(即迴圈體函數)會捕獲迴圈變數的不同副本,而不是都捕獲同一個迴圈變數。這裡說明瞭迴圈體函數中的迴圈變數不是簡單的引用,而是一個副本。
You Don't Know JS: Scope & Closures 中的理解:
Not only does let in the for-loop header bind the i to the for-loop body, but in fact, it re-binds it to each iteration of the loop, making sure to re-assign it the value from the end of the previous loop iteration.
let 不僅在頭部將
i
值綁定到for迴圈體中,事實上,let將i
重新綁定到每個迭代函數中,並確保將上一次迭代結束的結果重新賦值給i
這裡提到的子作用域(for迴圈的函數體{...}),其實準確地講叫詞法作用域(lexical scope),也被稱為靜態作用域。簡單地講就是在嵌套的函數組中,內部函數可以訪問父作用域的變數和其他資源。
結合上面的幾點可知,子作用域內用的還是外層聲明的i
變數,let i = 'abc';
就相當於在子作用域中聲明新的變數覆蓋了父作用域的變數聲明。但是子作用域內引用的這個父作用域變數不是直接引用,而是父作用域變數的一個副本,子作用域修改這個副本時,相當於修改父作用域變數,而父作用域迴圈變數改變時,不會影響子作用域內的副本變數,加粗的這句解釋說實話還是沒能說服我自己,所以我又找到了stackoverflow上的一個回答。
Why is let slower than var in a for loop in nodejs?雖然不是正面回答for迴圈的問題,但是裡面舉的一個Babel實現let的例子卻能從var的角度來解釋這個問題:
"use strict";
(function () {
var _loop = function _loop(_j) {
_j++; // here's the change inside the new scope
setTimeout(function () {
console.log("j: " + _j + " seconds");
}, _j * 1000);
j = _j; // here's the change being propagated back to maintain continuity
};
for (var j = 0; j < 5; j++) {
_loop(j);
}
})();
仔細看這個例子,外層定義的j
變數由形參_j
(這裡的形參傳值,就是動態作用域)傳入了迴圈體函數_loop()中,進入函數體中後,_j
就相當於他的副本,子作用域可以修改父作用域變數(表現在 j = _j),但_loop()函數執行結束後,父作用域變數j
的修改無法改變_loop()函數中的形參_j
,因為形參_j
只會在_loop()函數執行那一次被賦值,後面外層j
值的修改和他沒有關係。回想一下上面的問題,如果內部重新定義了j
值,那麼就會覆蓋外層傳進來的_j
(雖然在這個例子里j
和_j
變數名不一樣,但是在let聲明裡其實是同一個變數名),相當於子作用域定義了自己內部使用的變數,j = _j;
這樣的賦值語句也沒有意義了,因為這相當於變數自己給自己賦值。
上面這段話是從var實現let的角度來解釋,有點拗口。下麵說說我的理解,談談let變數是怎麼處理這個過程的:
for迴圈每次進入函數體{...}中,都是進入了新的子作用域中,每個子作用域相互獨立,新的子作用域引用(實際是變數複製)父作用域的迴圈值變數,同時可以修改變數的值且更新父作用域變數,實際表現就和真正引用了父作用域變數一樣。反之,父作用域無法訪問此複製變數,所以父作用域中變數的改變不會對子作用域中的變數有什麼影響。但是如果子作用域中重新聲明瞭此變數名,新的變數就綁定到了子作用域中,變成了子作用域的內部變數,覆蓋了父作用域的迴圈值變數,子作用域對新聲明的變數的修改都在子作用域範圍內,父作用域同樣無法訪問此變數。
小結
明白這些概念有時候感覺很繁雜,好像有點牛角尖,但是我覺得只有掌握正確的理解方向,才能夠根據實際情況去推斷、讀懂代碼,也有利於自己寫出規範化、易理解的代碼。這篇文章的內容依然是我理解思路的一個記錄,有點啰嗦,主要為了以後自己概念模糊後能夠找到現在思考的思路,由於其中有很多自己的理解,錯漏在所難免,也希望大家讀後能給我提出意見和建議。
本文來源:JuFoFu
本文地址:
參考文檔:
Jason Orendorff . ES6 In Depth: let and const
You-Dont-Know-JS . You Don't Know JS: Scope & Closures
Hammad Ahmed . Understanding Scope in JavaScript
What is the scope of variables in JavaScript?
Why is let slower than var in a for loop in nodejs?
Are variables declared with let or const not hoisted in ES6?
水平有限,錯誤歡迎指正。原創博文,轉載請註明出處。