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 從內存空間談到垃圾回收機制 這篇博客。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM