1. 程式人生 > 實用技巧 >for迴圈中let與var的區別

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 從記憶體空間談到垃圾回收機制這篇部落格。