說到ES6的let
變量聲明,我估計不少人會想起下面幾個主要的特色:javascript
不少教程和總結基本都說到了這幾點(說實話大部分文章都大同小異,摘錄的居多),習慣性我仍是去看了MDN上的文檔,立馬發現一個問題:html
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.java
ECMAScript 2015(即ES6),let會提高變量到代碼塊頂部。然而,在變量聲明前引用變量會致使ReferenceError錯誤。在代碼塊開始到變量聲明之間變量處於暫時死區(temporal dead zone)。
不得了,看來let是有變量聲明提高的啊,這個發現引發了個人興趣。我立馬去找了一些相關的資料查看,在查看的過程當中,我也慢慢了解了其餘一些隱含的容易誤解的知識點,下面羅列一些相關資料,方便讓有一樣興趣瞭解的童鞋去查閱:node
不肯意去翻閱資料的就看我下面的我的總結吧。git
關於變量聲明提高,有幾個重點:es6
在個人思路大概清晰寫這篇總結的時候,我又偶然在一篇講變量聲明提高的博文上看到一段MDN原文的引用:github
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在塊級做用域的正確表現就能夠了,理論仍是要爲實踐服務。閉包
說到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?,將這個過程描述得很清晰:ide
// 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,是由於看到了阮老師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
本文地址:http://www.cnblogs.com/JuFoFu/p/6726359.html
參考文檔:
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?