1. 程式人生 > 其它 >JavaScript——由for迴圈引發的關於var和作用域的思考

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語法的甜頭,以後應多使用新標準和新技術。
  • 反思:不該在糟粕的地方太過於鑽牛角尖,避免浪費時間。
javascript 閱讀 1.3k更新於 2019-09-24