2013-02-03 201 views
3

可能重複:
Javascript closure inside loops - simple practical example範圍規則

我與的setTimeout玩弄在我的一個項目,以節流元件的加入到DOM(所以用戶界面不會在頁面加載過程中凍結)。但是,我遇到了一些讓我感到困惑的事情。鑑於此代碼:

for(var i = 0; i < 5; i++) { 
    var j = i + 10; 
    console.log("i is: " + i + " j is: " + j); 
    setTimeout(function() { 
     console.log("in timeout i is: " + i + " j is: " + j); 
    }, i * 1000); 
} 

我得到以下輸出:

i is: 0 j is: 10 
i is: 1 j is: 11 
i is: 2 j is: 12 
i is: 3 j is: 13 
i is: 4 j is: 14 
in timeout i is: 5 j is: 14 
in timeout i is: 5 j is: 14 
in timeout i is: 5 j is: 14 
in timeout i is: 5 j is: 14 
in timeout i is: 5 j is: 14 

那的i的超時值爲5是顯而易見的,因爲我是在用於環路初始化作用域。但是,對於所有超時輸出,j是多少?我會認爲j在超時時間內會輸出10,11,12,13,14,因爲它在循環範圍內。我怎麼能達到這個結果?

+2

[JavaScript變量範圍](http://stackoverflow.com/questions/500431/javascript-variable-scope)。另請參見:[如何將JS變量的值*傳遞給函數?](http://stackoverflow.com/questions/2568966/how-do-i-pass-the-value-not-the-參考-JS-variable-to-a-function) – DCoder

+0

極好的參考!我以爲JavaScript有一個塊範圍,但顯然我只是在思考JavaScript的錯誤導致將是理智的;) –

+0

編輯我的答案,實際回答你的問題,看最後一個片段在底部。 –

回答

7

那是因爲,在JavaScript中,var函數範圍

var聲明將被懸掛到當前執行上下文的頂部。也就是說,如果它位於函數內部,var將在函數的執行上下文範圍內,否則是程序(全局)執行上下文。

ECMAScript 2015(又名ES6)引入了let它可以讓你創建塊範圍變量,但是因爲它沒有得到廣泛的支持,所以我只是留下鏈接供參考。

一種變通方法,仍用var,並將它「範圍的」環路內,是創建一個新的執行上下文,也知道作爲closure

function callbackFactory(i, j) { 
    // Now `i` and `j` are scoped inside each `callbackFactory` execution context. 
    return function() { // This returned function will be used by the `setTimeout`. 
     // Lexical scope (scope chain) will seek up the closest `i` and `j` in parent 
     // scopes, that being of the `callbackFactory`'s scope in which this returned 
     // function has been initialized. 
     console.log("in timeout i is: " + i + " j is: " + j); 
    }; 
} 
for(var i = 0; i < 5; i++) { 
    var j = i + 10; 
    console.log("i is: " + i + " j is: " + j); 
    setTimeout(callbackFactory(i, j), i * 1000); 
} 

至於我作用域都ij內回調範圍內,它們將返回setTimeout內的相同值,而不是它們傳遞給callbackFactory時的值。

請參閱Live demo

做同樣事情的另一種方法是在for循環內創建一個IIFE。這通常比較容易閱讀,但JS(H | L)int會對你大喊。 ;)這是因爲在循環內創建函數被認爲性能不好。

for(var i = 0; i < 5; i++) { 
    var j = i + 10; 
    console.log("i is: " + i + " j is: " + j); 
    (function(i, j) { // new execution context created for each iteration 
     setTimeout(function() { 
      console.log("in timeout i is: " + i + " j is: " + j); 
     }, i * 1000); 
    }(i, j)); // the variables inside the `for` are passed to the IIFE 
} 

上面我在每次迭代中創建的for內一個新的執行上下文。 (Demo

混合上述IIFE第一種方法(callbackFactory),我們甚至可以做一個第三選擇:

for(var i = 0; i < 5; i++) { 
    var j = i + 10; 
    console.log("i is: " + i + " j is: " + j); 
    setTimeout(function(i, j) { 
     return function() { 
      console.log("in timeout i is: " + i + " j is: " + j); 
     }; 
    }(i, j), i * 1000); 
} 

這簡直是使用IIFE在callbackFactory功能的地方。這看起來不是很容易閱讀,但仍在for循環內創建功能,這對性能不利,但只是注意到這也是可能的,並且works

這3種方法在野外很常見。 =]


哦,差點忘了回答主要問題。只要把callbackFactory在同一範圍內for循環,然後代替範圍界定它內部的i,讓作用域鏈尋求外部範圍的i

(function() { 
    var i, j; 
    function callbackFactory(j) { 
    // the `j` inside this execution context enters it as a formal parameter, 
    // shadowing the outer `j`. That is, it is independent from the outer `j`. 
    // You could name the parameter as "k" and use "k" when logging, for example. 
     return function() { 
      // Scope chain will seek the closest `j` in parent scopes, that being 
      // the one from the callbackFactory's scope in which this returned 
      // function has been initialized. 
      // It will also seek up the "closest" `i`, 
      // which is scoped inside the outer wrapper IIFE. 
      console.log("in timeout i is: " + i + " j is: " + j); 
     }; 
    } 
    for(i = 0; i < 5; i++) { 
     j = i + 10; 
     console.log("i is: " + i + " j is: " + j); 
     setTimeout(callbackFactory(j), i * 1000); 
    } 
}()); 
/* Yields: 
i is: 0 j is: 10 
i is: 1 j is: 11 
i is: 2 j is: 12 
i is: 3 j is: 13 
i is: 4 j is: 14 
in timeout i is: 5 j is: 10 
in timeout i is: 5 j is: 11 
in timeout i is: 5 j is: 12 
in timeout i is: 5 j is: 13 
in timeout i is: 5 j is: 14 */ 

Fiddle

注意,我我已經將ij聲明移到了範圍的頂部,僅爲了便於閱讀。它與for (var i = [...]具有相同的效果,將由翻譯人員提起。