for迴圈中let與var的區別
轉載於:https://www.cnblogs.com/echolun/p/10584703.html
一、一個簡單的for迴圈問題與我思考後產生的問題
還是這段程式碼,分別用var與let去宣告變數,得到的卻是完全不同的結果,為什麼?如果讓你把這個東西清晰的講給別人聽,怎麼去描述呢?
//使用var宣告,得到3個3 var a = []; for (var i = 0; i < 3; i++) { a[i] = function () { console.log(i); }; } a[0](); //3 a[1](); //3 a[2](); //3 //使用let宣告,得到0,1,2 var a = []; for (let i = 0; i < 3; i++) { a[i] = function () { console.log(i); }; } a[0](); //0 a[1](); //1 a[2](); //2
再弄懂這個問題前,我們得知道for迴圈是怎麼執行的。首先,對於一個for迴圈,設定迴圈變數的地方是一個父作用域,而迴圈體程式碼在一個子作用域內;別忘了for迴圈還有條件判斷,與迴圈變數的自增。
for循序的執行順序是這樣的:設定迴圈變數(var i = 0)==》迴圈判斷(i<3)==》滿足執行迴圈體==》迴圈變數自增(i++)
我們按照這個邏輯改寫上面的for迴圈,以第一個var宣告為例,結合父子作用域的特點,上面的程式碼可以理解為:
{ //我是父作用域 var i = 0; if (0 < 3) { a[0] = function () { //我是子作用域 console.log(i); }; }; i++; //為1 if (1 < 3) { a[1] = function () { console.log(i); }; }; i++; //為2 if (2 < 3) { a[2] = function () { console.log(i); }; }; i++; //為3 // 跳出迴圈 } //呼叫N次指向都是最終的3 a[0](); //3 a[1](); //3 a[2](); //3
那麼我們此時模擬的步驟程式碼中的宣告方式var修改為let,執行程式碼,發現輸出的還是3個3!WTF???
按照模糊的理解,當for迴圈使用let時產生了塊級作用域,每次迴圈塊級作用域中的 i 都相互獨立,並不像var那樣全程共用了一個。
但是有個問題,子作用域中並沒有let,何來的塊級作用域,整個迴圈也就父作用域中使用了一次let i = 0;子作用域哪裡來的塊級作用域?
請教了下百度的同學,談到了會不會是迴圈變數不止聲明瞭一次,其實自己也考慮到了這個問題,for迴圈會不會因為使用let而改變了我們常規理解的執行順序,自己又在子作用域用了let從而創造了塊級作用域?抱著僥倖的心理還是打斷點測試了一下:
可以看到,使用let還是一樣,宣告只有一次,之後就在後三個步驟中來回跳動了。
二、一個額外問題的暗示
如果說,在使用let的情況下產生了塊級作用域,每次迴圈的i都是獨立的一份,並不共用,那有個問題,第二次迴圈i++自增時又是怎麼知道上一個塊級作用域中的 i 是多少的。這裡得到的解釋是從阮一峰ES6入門獲取的。
JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數
i
時,就在上一輪迴圈的基礎上進行計算。
那這就是JS引擎底層實現的問題了,我還真沒法用自己的程式碼去模擬去實現,我們分別截圖var與let斷點情況下作用域的分佈。
首先是var宣告時,當函式執行時,只能在全域性作用域中找到已被修改的變數i,此時已被修改為3
而當我們使用let宣告時,子作用域本沒有使用let,不應該是是塊級作用域,但斷點顯示卻是一個block作用域,而且可以確定的是整個for迴圈let i只聲明瞭一次,但產生了三個塊級作用域,每個作用域中的 i 均不相同。
那在子作用域中,我並沒有使用let,這個塊級作用域哪裡開的,從JS引擎記錄 i 的變換進行迴圈自增而我們卻無法感知一樣,我猜測,JS引擎在let的情況下,每次迴圈自己都建立了一個塊級作用域並塞到了for迴圈裡(畢竟子作用域裡沒用let),所以才有了三次迴圈三個獨立的塊級作用域以及三個獨立的 i。
這也只是我的猜測了,可能不對,如果有人能從JS底層實現給我解釋就更好了。
PS:2019.4.19更新
之前對於for迴圈中使用let的步驟拆分推斷,我們得到了兩個結論以及一個猜想:
結論1:在for迴圈中使用let的情況下,由於塊級作用域的影響,導致每次迭代過程中的 i 都是獨立的存在。
結論2:既然說每次迭代的i都是獨立的存在,那i自增又是怎麼知道上次迭代的i是多少?這裡通過ES6提到的,我們知道是js引擎底層進行了記憶。
猜測1:由於整個for迴圈的執行體中並沒有使用let,但是執行中每次都產生了塊級作用域,我猜想是由底層程式碼建立並塞給for執行體中。
由於寫這篇部落格的時候順便給同事講了let相關知識,同事今天也正好看了模擬底層實現的程式碼,這個做個補充:
還是上面的例子,我們在let情況下對for迴圈步驟拆分,程式碼如下:
var a = []; { //我是父作用域 let i = 0; if (i < 3) { //這一步模擬底層實現 let k = i; a[k] = function () { //我是子作用域 console.log(k); }; }; i++; //為1 if (i < 3) { let k = i; a[k] = function () { console.log(k); }; }; i++; //為2 if (i < 3) { let k = i; a[k] = function () { console.log(k); }; }; i++; //為3 // 跳出迴圈 } a[0](); //0 a[1](); //1 a[2](); //2
上述程式碼中,每次迭代新增了let k = i這一步,且這一步由底層程式碼實現,我們看不到;
這一行程式碼起到兩個作用,第一是產生了塊級作用域,解釋了這個塊級作用域是怎麼來的,由於塊級的作用,導致3個k互不影響。
第二是通過賦值的行為讓3個k都訪問外部作用域的i,讓三個k建立了聯絡,這也解釋了自增時怎麼知道上一步是多少。
這篇文章有點鑽牛角尖了,不過有個問題在心頭不解決是真的難受,大概如此了。
PS:2019.11.28更新
謝謝博友coltfoal在基本資料型別與引用資料的概念上提供了一個有趣的例子,程式碼如下,猜猜輸出什麼:
var a = [] for (let y = {i: 0}; y.i < 3; y.i++) { a[y.i] = function () { console.log(y.i); }; }; a[0](); a[1](); a[2]();
你一定會好奇,為什麼輸出的是3個3,不是說let會建立一個塊級作用域嗎,我們還是一樣的改成寫模擬程式碼,如下:
var a = []; { //我是父作用域 let y = { i: 0 }; if (y.i < 3) { //這一步模擬底層實現 let k = y; a[k.i] = function () { //我是子作用域 console.log(k.i); }; }; y.i++; //為1 if (y.i < 3) { let k = y; a[k.i] = function () { console.log(k.i); }; }; y.i++; //為2 if (y.i < 3) { let k = y; a[k.i] = function () { console.log(k.i); }; }; y.i++; //為3 // 跳出迴圈 } a[0](); //3 a[1](); //3 a[2](); //3
注意,在模擬程式碼中為let k = y而非let k = y.i。我們始終使用let宣告一個新變數用於儲存for迴圈中的初始變數y,以達到建立塊級作用域的目的,即使y是一個物件。
那為什麼有了塊級作用域,最終結果還是相同呢,這就涉及了深/淺拷貝的問題。由於y屬於引用資料型別,let k = y 本質上是儲存了變數 y 指向值的引用地址,當迴圈完畢時,y中的 i 已自增為3。
變數k因為塊級作用域的原因雖然也是三個不同的k,但不巧的是大家儲存的是同一個引用地址,所以輸出都是3了。
我們再次改寫程式碼,說說會輸出什麼:
var a = [] var b = {i:0}; for (let y = b.i; y < 3; y++) { a[y] = function () { console.log(y); }; }; a[0](); a[1](); a[2]();
對深/淺拷貝有疑問可以閱讀博主這篇部落格深拷貝與淺拷貝的區別,實現深拷貝的幾種方法
若對JavaScript中基本資料型別,引用資料型別的儲存有興趣,可以閱讀JS 從記憶體空間談到垃圾回收機制這篇部落格。