JavaScript——由for迴圈引發的關於var和作用域的思考
JavaScript——由for迴圈引發的關於var和作用域的思考
Oliver釋出於 2019-09-16由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語言的我是一個觀念上的顛覆,於是開始了大規模的資料查詢,試圖解決我的這個疑惑。
Stage 2
- 在經過幾番詢問和一些技術部落格的翻閱之後,得到了如下的一種解釋:
"在這個函式裡面的i其實引用的是最後一次i的值,為什麼不是1,2,3,4...呢? 因為for迴圈中並沒有執行這個函式,這個函式是在你點選的時候才執行的,當執行這個函式的時候,它發現它自己沒有這個變數i,於是向它的作用域鏈中查詢這個變數i,因為當你單擊這個box的時候已經for迴圈完了,所以找到的i是最後一次賦值後的i"
- 本以為事情到此結束了,可我感覺還是差了些什麼,下面這篇部落格解開了我心中的最彆扭的結。
引用自:https://www.cnblogs.com/qiegu...
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(); 才會執行函式內部的程式碼。上面程式碼翻譯一下就是:
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。”
- 想法:繫結的函式並不是立刻就實現,而是處於等待呼叫的狀態。當程式的執行流進入一個函式的時候,這個函式被推入一個環境棧中,再進行變數讀取和函式內容的實現。
- 例項化地,在本篇開頭的程式碼中,五次迴圈將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)值,形成了單獨的作用域鏈。
- es6中let方法:
function fun(){
for(let i=0; i<lis.length; i++){ //此處的length=5
lis[i].onclick = function(){
console.log(i);
}
}
}
雖然本人還沒有正式開始es6的學習(捂臉,但因涉及本篇部落格的解決方法,還是認真地瞭解了一下let關鍵字。此方法的成功,最大的功臣便是let的塊級作用域特點,他在每次迴圈中生成了單獨的作用域,達到了與上一種方法相同的效果。
Stage 5
研究這個看似很簡單的特性耗費了整整一天的時間,也深深體會到了為什麼說JS語言的糟粕不少。
-
總結:
- for迴圈體內定義函式 ,若函式體內用了for塊內的var變數,在for語句外呼叫該函式時,該函式採用的是迴圈結束後的var值
- 而塊內用let變數,與之同級的函式體用了該let變數,之後呼叫函式,函式使用的是定義時塊內的let變數值。
- 收貨:更加明確了關於作用域、閉包等概念。嚐到了ES6語法的甜頭,以後應多使用新標準和新技術。
- 反思:不該在糟粕的地方太過於鑽牛角尖,避免浪費時間。