JavaScript——由for循環引起的關於var和做用域的思考

由for循環引起的關於var和做用域的思考

Stage 1

  • 原由是在某技術博客裏看到了以下代碼
function fun(){
    for(var i=0; i<lis.length; i++){    //此處的length=5
      lis[i].onclick = function(){
        console.log(i);
      }
    }
}
複製代碼

因而我在console裏寫入瞭如上代碼,依次點擊lis,輸出了五次4,這對於寫慣了c語言的我是一個觀念上的顛覆,因而開始了大規模的資料查找,試圖解決個人這個疑惑。html

Stage 2

  • 在通過幾番詢問和一些技術博客的翻閱以後,獲得了以下的一種解釋:

"在這個函數裏面的i其實引用的是最後一次i的值,爲何不是1,2,3,4...呢? 由於for循環中並無執行這個函數,這個函數是在你點擊的時候才執行的,當執行這個函數的時候,它發現它本身沒有這個變量i,因而向它的做用域鏈中查找這個變量i,由於當你單擊這個box的時候已經for循環完了,因此找到的i是最後一次賦值後的i"es6

  • 本覺得事情到此結束了,可我感受仍是差了些什麼,下面這篇博客解開了我心中的最彆扭的結。 引用自:www.cnblogs.com/qieguo/p/54…
function createFunctions(){
    var result = new Array();
    for (var i=0; i < 10; i++){
        result[i] = function(){
            return i;
        };
    }
    return result;
}
var funcs = createFunctions();
for (var i=0; i < funcs.length; i++){
    console.log(funcs[i]());
}
複製代碼

陷阱就是:函數帶()纔是執行函數! 單純的一句 var f = function() { alert('Hi'); }; 是不會彈窗的,後面接一句 f(); 纔會執行函數內部的代碼。上面代碼翻譯一下就是:bash

var result = new Array(), i;
result[0] = function(){ return i; }; //沒執行函數,函數內部不變,不能將函數內的i替換!
result[1] = function(){ return i; }; //沒執行函數,函數內部不變,不能將函數內的i替換!
...
result[9] = function(){ return i; }; //沒執行函數,函數內部不變,不能將函數內的i替換!
i = 10;
funcs = result;
result = null;

console.log(i); // funcs[0]()就是執行 return i 語句,就是返回10
console.log(i); // funcs[1]()就是執行 return i 語句,就是返回10
...
console.log(i); // funcs[9]()就是執行 return i 語句,就是返回10
複製代碼

"爲何只垃圾回收了 result,但卻不收了 i 呢? 由於 i 還在被 function 引用着啊。比如一個餐廳,盤子老是有限的,因此服務員會去巡臺回收空盤子,但還裝着菜的盤子他怎麼敢收? 固然,你本身手動倒掉了盤子裏面的菜(=null),那盤子就會被收走了,這就是所謂的內存回收機制。"閉包

Stage 3

  • 在《JavaScript高級程序設計》的7.2節終於鞏固了個人理解:

「做用域鏈的機制引出了一個值得注意的反作用,即閉包只能取得包含函數中任何變量的最後一個值。」函數

「表面上看,每一個函數都應該返回本身對應的i值,但實際上每一個函數都返回了同樣的值。由於每一個函數的做用域鏈中都保存着fun()函數的活動對象,因此他們引用的都是同一個變量i。當fun()函數返回後,變量的i值是4,此時每一個函數都引用着保存變量i的同一個變量對象,因此在每一個函數內部i的值都是10。」ui

  • 想法:綁定的函數並非馬上就實現,而是處於等待調用的狀態。當程序的執行流進入一個函數的時候,這個函數被推入一個環境棧中,再進行變量讀取和函數內容的實現。
  • 實例化地,在本篇開頭的代碼中,五次循環將lis[i].onclick事件分別綁定在了五個匿名函數上,開闢了五個執行環境,進而造成了五條做用域鏈,形如:[閉包]→[fun()的活動對象]→[全局變量對象],而很容易理解地,fun()活動對象是這五條做用域鏈所共享的,天然i值也就是共享的了

Stage 4

  • 這部分該講講解決方法了
  • 高程上推薦的方法:經過建立另外一個匿名函數強制讓閉包行爲符合預期
function fun(){
    for(var i=0; i<lis.length; i++){    //此處的length=5
      lis[i].onclick = (function(num){
          return function(){
            console.log(num);
          }
      })(i)
    }
}
複製代碼

這種方法在每次循環中,用當即執行的匿名函數記錄下了當前的i值(num),並建立了單獨的做用域,又在匿名函數中建立了一個新的閉包,接收i(num)值,造成了單獨的做用域鏈。spa

  • es6中let方法:
function fun(){
    for(let i=0; i<lis.length; i++){    //此處的length=5
        lis[i].onclick = function(){
            console.log(i);
        }
    }
}

複製代碼

此方法的成功,最大的功臣即是let的塊級做用域特色,他在每次循環中生成了單獨的做用域,達到了與上一種方法相同的效果。翻譯

Stage 5

研究這個看似很簡單的特性耗費了整整一天的時間,也深深體會到了爲何說JS語言的糟粕很多。設計

  • 總結:
    • for循環體內定義函數 ,若函數體內用了for塊內的var變量,在for語句外調用該函數時,該函數採用的是循環結束後的var值
    • 而塊內用let變量,與之同級的函數體用了該let變量,以後調用函數,函數使用的是定義時塊內的let變量值。
  • 收貨:更加明確了關於做用域、閉包等概念。嚐到了ES6語法的甜頭,之後應多使用新標準和新技術。
  • 反思:不應在糟粕的地方太過於鑽牛角尖,避免浪費時間。
相關文章
相關標籤/搜索