1. 程式人生 > >深入理解閉包系列第四篇——常見的一個迴圈和閉包的錯誤詳解

深入理解閉包系列第四篇——常見的一個迴圈和閉包的錯誤詳解

前面的話

  關於常見的一個迴圈閉包的錯誤,很多資料對此都有文字解釋,但還是難以理解。本文將以執行環境圖示的方式來對此進行更直觀的解釋,以及對此類需求進行推衍,得到更合適的解決辦法

犯錯

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//
2

  以上程式碼的執行結果是2,而不是預想的0。接下來用執行環境圖示的方法,詳解到底是哪裡出了問題

  執行流首先建立並進入全域性執行環境,進行宣告提升過程。執行流執行到第10行,建立並進入foo()函式執行環境,並進行宣告提升。然後執行第2行,將arr賦值為[]。然後執行第3行,給arr[0]和arr[1]都賦值為一個匿名函式。然後執行第8行,以arr的值為返回值退出函式。由於此時有閉包的存在,所以foo()執行環境並不會被銷燬

  執行流進入全域性執行環境,繼續執行第10行,將函式的返回值arr賦值給bar

  執行流執行第11行,訪問bar的第0個元素並執行。此時,執行流建立並進入匿名函式執行環境,匿名函式中存在

自由變數i,需要使用其作用域鏈匿名函式 -> foo()函式 -> 全域性作用域進行查詢,最終在foo()函式的作用域找到了i,然後在foo()函式的執行環境中找到了i的值2,於是給i賦值2

  執行流接著執行第5行,以i的值2作為返回值返回。同時銷燬匿名函式的執行環境。執行流進入全域性執行環境,接著執行第11行,呼叫內部物件console,並找到其方法log,將bar[0]()的值2作為引數放入該方法中,最終在控制檯顯示2

   由此我們看出,犯錯原因是在迴圈的過程中,並沒有把函式的返回值賦值給陣列元素,而僅僅是把函式賦值給了陣列元素。這就使得在呼叫匿名函式時,通過作用域找到的執行環境中儲存的變數的值已經不是迴圈時的瞬時索引值,而是迴圈執行完畢之後的索引值

IIFE

  由此,可以利用IIFE傳參和閉包來建立多個執行環境來儲存迴圈時各個狀態的索引值。因為函式傳參是按值傳遞的,不同引數的函式被呼叫時,會建立不同的執行環境

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = (function fn(j){
            return function test(){
                return j;
            }
        })(i);
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//0    

塊作用域

  使用IIFE還是較為複雜,使用塊作用域則更為方便

  由於塊作用域可以將索引值i重新繫結到了迴圈的每一個迭代中,確保使用上一個迴圈迭代結束時的值重新進行賦值,相當於為每一次索引值都建立一個執行環境

function foo(){
    var arr = [];
    for(let i = 0; i < 2; i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//0    

最後

  在程式設計中,如果實際和預期結果不符,就按照程式碼順序一步一步地把執行環境圖示畫出來,會發現很多時候就是在想當然

  以上