什麼是closure - 循環內的JavaScript閉包-簡單實用的例子
js closure scope (20)
我們將檢查,當您聲明
var
並let
逐個實際發生時。
案例1:使用var
<script>
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () {
debugger;
console.log("My value: " + i);
};
}
console.log(funcs);
</script>
現在按F12打開Chrome控制台窗口並刷新頁面。在數組中擴展每3個函數。您將看到一個名為.Expand 的屬性。您將看到一個被調用的數組對象,展開該對象。您將找到一個聲明為對象的屬性,其值為3。[[Scopes]]
"Global"
'i'
結論:
- 當您
'var'
在函數外部聲明變量時,它將變為全局變量(您可以通過鍵入i
或window.i
在控制台窗口中進行檢查。它將返回3)。 - 除非您調用函數,否則您聲明的不可靠函數將不會調用並檢查函數內的值。
- 調用該函數時,
console.log("My value: " + i)
從其對Global
像中獲取值並顯示結果。
CASE2:使用let
現在更換'var'
用'let'
<script>
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function () {
debugger;
console.log("My value: " + i);
};
}
console.log(funcs);
</script>
做同樣的事情,轉到範圍。現在你將看到兩個對象"Block"
和"Global"
。現在展開Block
對象,你會看到'i'在那裡被定義,奇怪的是,對於每個函數,值if i
是不同的(0,1,2)。
結論:
當你使用'let'
函數外部但在循環內部聲明變量時,這個變量將不是一個全局變量,它將成為一個Block
只能用於同一個函數的級別變量。這就是我們獲得i
不同價值的原因。當我們調用函數時為每個函數。
有關近距離工作的更多細節,請瀏覽精彩的視頻教程https://youtu.be/71AtaJpJHw0
var funcs = [];
for (var i = 0; i < 3; i++) { // let's create 3 functions
funcs[i] = function() { // and store them in funcs
console.log("My value: " + i); // each should log its value.
};
}
for (var j = 0; j < 3; j++) {
funcs[j](); // and now let's run each one to see
}
它輸出:
我的價值:3
我的價值:3
我的價值:3
而我希望它輸出:
我的價值:0
我的價值:1
我的價值:2
使用事件偵聽器導致運行函數的延遲時,會出現同樣的問題:
var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) { // let's create 3 functions
buttons[i].addEventListener("click", function() { // as event listeners
console.log("My value: " + i); // each should log its value.
});
}
<button>0</button><br>
<button>1</button><br>
<button>2</button>
...或異步代碼,例如使用Promises:
// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));
for(var i = 0; i < 3; i++){
wait(i * 100).then(() => console.log(i)); // Log `i` as soon as each promise resolves.
}
這個基本問題的解決方案是什麼?
試試這個較短的一個
沒有數組
沒有額外的循環
for (var i = 0; i < 3; i++) {
createfunc(i)();
}
function createfunc(i) {
return function(){console.log("My value: " + i);};
}
OP顯示的代碼的主要問題是,直到第二個循環才會讀取i
。 為了演示,想像一下在代碼中看到錯誤
funcs[i] = function() { // and store them in funcs
throw new Error("test");
console.log("My value: " + i); // each should log its value.
};
在執行funcs[someIndex]
之前,實際上不會發生錯誤。 使用相同的邏輯,顯然在此之前也不會收集i
的值。 一旦原始循環結束, i++
將i
帶到值3
,這導致條件i < 3
失敗並且循環結束。 在這一點上, i
是3
,所以當使用funcs[someIndex]()
, i
被評估,每次都是3。
為了解決這個問題,您必須在遇到問題時對其進行評估。 請注意,這已經以funcs[i]
的形式發生(其中有3個唯一索引)。 有幾種方法可以捕獲此值。 一種是將其作為參數傳遞給一個函數,該函數已經以幾種方式顯示在這裡。
另一個選擇是構造一個能夠關閉變量的函數對象。 這樣就可以實現
funcs[i] = new function() {
var closedVariable = i;
return function(){
console.log("My value: " + closedVariable);
};
};
反對是一個原始人
讓我們定義回調函數如下:
// ****************************
// COUNTER BEING A PRIMITIVE
// ****************************
function test1() {
for (var i=0; i<2; i++) {
setTimeout(function() {
console.log(i);
});
}
}
test1();
// 2
// 2
超時完成後,將為兩者打印2。這是因為回調函數基於詞法範圍訪問該值,其中定義了函數。
要在定義回調時傳遞和保留值,我們可以創建一個closure,以在調用回調之前保留該值。這可以按如下方式完成:
function test2() {
function sendRequest(i) {
setTimeout(function() {
console.log(i);
});
}
for (var i = 0; i < 2; i++) {
sendRequest(i);
}
}
test2();
// 1
// 2
現在有什麼特別之處是“原語是通過值傳遞並複制的。因此,當定義閉包時,它們會保留前一循環的值。”
反對一個對象
由於閉包可以通過引用訪問父函數變量,因此這種方法與基元的方法不同。
// ****************************
// COUNTER BEING AN OBJECT
// ****************************
function test3() {
var index = { i: 0 };
for (index.i=0; index.i<2; index.i++) {
setTimeout(function() {
console.log('test3: ' + index.i);
});
}
}
test3();
// 2
// 2
因此,即使為作為對像傳遞的變量創建了閉包,也不會保留循環索引的值。這是為了表明不會復制對象的值,而是通過引用訪問它們。
function test4() {
var index = { i: 0 };
function sendRequest(index, i) {
setTimeout(function() {
console.log('index: ' + index);
console.log('i: ' + i);
console.log(index[i]);
});
}
for (index.i=0; index.i<2; index.i++) {
sendRequest(index, index.i);
}
}
test4();
// index: { i: 2}
// 0
// undefined
// index: { i: 2}
// 1
// undefined
使用立即調用的函數表達式 ,這是封裝索引變量的最簡單,最易讀的方法:
for (var i = 0; i < 3; i++) {
(function(index) {
console.log('iterator: ' + index);
//now you can also loop an ajax call here
//without losing track of the iterator value: $.ajax({});
})(i);
}
這將迭代器i
發送到我們定義為index
的匿名函數中。 這將創建一個閉包,其中變量i
被保存以供稍後在IIFE中的任何異步功能中使用。
借助ES6的新功能,可以管理塊級別範圍:
var funcs = [];
for (let i = 0; i < 3; i++) { // let's create 3 functions
funcs[i] = function() { // and store them in funcs
console.log("My value: " + i); // each should log its value.
};
}
for (let j = 0; j < 3; j++) {
funcs[j](); // and now let's run each one to see
}
OP問題中的代碼替換let
為var
。
另一種說法是,函數中的i
在執行函數時受到約束,而不是創建函數的時間。
創建閉包時, i
是對外部作用域中定義的變量的引用,而不是創建閉包時的副本。 它將在執行時進行評估。
大多數其他答案提供了通過創建另一個不會為您更改值的變量來解決的方法。
我想我會添加一個清晰的解釋。 對於解決方案,個人而言,我會選擇Harto,因為從這裡的答案來看,這是最明智的方式。 發布的任何代碼都可以使用,但是我選擇封閉工廠而不必編寫一堆註釋來解釋為什麼我要聲明一個新變量(Freddy和1800's)或者有奇怪的嵌入式閉包語法(apphacker)。
嘗試:
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = (function(index) {
return function() {
console.log("My value: " + index);
};
}(i));
}
for (var j = 0; j < 3; j++) {
funcs[j]();
}
編輯 (2014):
我個人認為@ Aust 最近關於使用.bind
答案是現在做這種事情的最好方法。 當你不需要或想要搞亂bind
的_.partial
時,還有lo-dash / underscore的thisArg
。
您的代碼不起作用,因為它的作用是:
Create variable `funcs` and assign it an empty array;
Loop from 0 up until it is less than 3 and assign it to variable `i`;
Push to variable `funcs` next function:
// Only push (save), but don't execute
**Write to console current value of variable `i`;**
// First loop has ended, i = 3;
Loop from 0 up until it is less than 3 and assign it to variable `j`;
Call `j`-th function from variable `funcs`:
**Write to console current value of variable `i`;**
// Ask yourself NOW! What is the value of i?
現在的問題是,i
調用函數時變量的值是多少?因為第一個循環是在條件為的情況下創建的i < 3
,所以當條件為假時它會立即停止,所以它就是i = 3
。
您需要了解,在創建函數時,沒有執行任何代碼,只會保存以供日後使用。因此,當稍後調用它們時,解釋器會執行它們並詢問:“當前值是i
多少?”
所以,你的目標是首先保存i
to function 的值,然後才保存函數funcs
。這可以通過以下方式完成:
var funcs = [];
for (var i = 0; i < 3; i++) { // let's create 3 functions
funcs[i] = function(x) { // and store them in funcs
console.log("My value: " + x); // each should log its value.
}.bind(null, i);
}
for (var j = 0; j < 3; j++) {
funcs[j](); // and now let's run each one to see
}
這樣,每個函數都有自己的變量x
,我們將其設置x
為i
每次迭代的值。
這只是解決此問題的多種方法之一。
最簡單的解決方案是,
而不是使用:
var funcs = [];
for(var i =0; i<3; i++){
funcs[i] = function(){
alert(i);
}
}
for(var j =0; j<3; j++){
funcs[j]();
}
提醒“2”,共3次。 這是因為在for循環中創建的匿名函數共享相同的閉包,並且在該閉包中, i
的值是相同的。 使用它來防止共享關閉:
var funcs = [];
for(var new_i =0; new_i<3; new_i++){
(function(i){
funcs[i] = function(){
alert(i);
}
})(new_i);
}
for(var j =0; j<3; j++){
funcs[j]();
}
這背後的想法是,使用IIFE (立即調用的函數表達式)封裝for循環的整個主體,並將new_i
作為參數傳遞new_i
其捕獲為i
。 由於匿名函數是立即執行的,因此對於匿名函數內定義的每個函數, i
值都是不同的。
這個解決方案似乎適合任何這樣的問題,因為它需要對遇到此問題的原始代碼進行最小的更改。 事實上,這是設計,它應該不是一個問題!
這描述了在JavaScript中使用閉包的常見錯誤。
函數定義新環境
考慮:
function makeCounter()
{
var obj = {counter: 0};
return {
inc: function(){obj.counter ++;},
get: function(){return obj.counter;}
};
}
counter1 = makeCounter();
counter2 = makeCounter();
counter1.inc();
alert(counter1.get()); // returns 1
alert(counter2.get()); // returns 0
每次調用makeCounter
, {counter: 0}
導致創建一個新對象。 此外,還創建了一個新的obj
副本以引用新對象。 因此, counter1
和counter2
彼此獨立。
循環中的閉包
在循環中使用閉包很棘手。
考慮:
var counters = [];
function makeCounters(num)
{
for (var i = 0; i < num; i++)
{
var obj = {counter: 0};
counters[i] = {
inc: function(){obj.counter++;},
get: function(){return obj.counter;}
};
}
}
makeCounters(2);
counters[0].inc();
alert(counters[0].get()); // returns 1
alert(counters[1].get()); // returns 1
請注意, counters[0]
和counters[1]
不是獨立的。 事實上,他們在同一個obj
上運作!
這是因為在循環的所有迭代中只有一個obj
副本,可能是出於性能原因。 即使{counter: 0}
在每次迭代中創建一個新對象, obj
的相同副本也會通過對最新對象的引用進行更新。
解決方案是使用另一個輔助函數:
function makeHelper(obj)
{
return {
inc: function(){obj.counter++;},
get: function(){return obj.counter;}
};
}
function makeCounters(num)
{
for (var i = 0; i < num; i++)
{
var obj = {counter: 0};
counters[i] = makeHelper(obj);
}
}
這是有效的,因為函數作用域中的局部變量以及函數參數變量在進入時都會分配新的副本。
有關詳細討論,請參閱JavaScript閉包陷阱和用法
這是一個使用forEach
的簡單解決方案(回到IE9):
var funcs = [];
[0,1,2].forEach(function(i) { // let's create 3 functions
funcs[i] = function() { // and store them in funcs
console.log("My value: " + i); // each should log its value.
};
})
for (var j = 0; j < 3; j++) {
funcs[j](); // and now let's run each one to see
}
打印:
My value: 0 My value: 1 My value: 2
隨著ES6現在得到廣泛支持,這個問題的最佳答案已經改變。 ES6為這種確切的環境提供了let
和const
關鍵字。 我們可以使用let
來設置如下的循環範圍變量,而不是搞亂閉包。
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}
然後, val
將指向特定於循環的特定轉彎的對象,並且將返回正確的值而不使用額外的閉包表示法。 這顯然簡化了這個問題。
const
類似於let
帶有額外的限制,即在初始賦值後變量名不能反彈到新的引用。
現在,瀏覽器支持針對最新版本的瀏覽器。 目前最新的Firefox,Safari,Edge和Chrome都支持const
/ let
。 Node也支持它,你可以利用像Babel這樣的構建工具在任何地方使用它。 你可以在這裡看到一個有效的例子: http://jsfiddle.net/ben336/rbU4t/2/ : http://jsfiddle.net/ben336/rbU4t/2/
文件在這裡:
但是要注意,在Edge 14支持之前IE9-IE11和Edge let
但卻出現上述錯誤(它們不會每次都創建一個新的i
,因此上面的所有函數都會記錄3,就像我們使用var
)。 Edge 14最終做對了。
首先,了解這段代碼的錯誤:
var funcs = [];
for (var i = 0; i < 3; i++) { // let's create 3 functions
funcs[i] = function() { // and store them in funcs
console.log("My value: " + i); // each should log its value.
};
}
for (var j = 0; j < 3; j++) {
funcs[j](); // and now let's run each one to see
}
這里當funcs[]
數組被初始化時,i
正在遞增,funcs
數組被初始化並且func
數組的大小變為3,所以i = 3,
。現在,當funcs[j]()
調用它時,它再次使用變量i
,該變量已經增加到3。
現在要解決這個問題,我們有很多選擇。以下是其中兩個:
我們可以初始化
i
用let
或初始化一個新的變量index
與let
和使其等於i
。因此,在進行調用時,index
將使用它並且其範圍將在初始化後結束。對於呼叫,index
將再次初始化:var funcs = []; for (var i = 0; i < 3; i++) { let index = i; funcs[i] = function() { console.log("My value: " + index); }; } for (var j = 0; j < 3; j++) { funcs[j](); }
其他選項可以引入一個
tempFunc
返回實際函數:var funcs = []; function tempFunc(i){ return function(){ console.log("My value: " + i); }; } for (var i = 0; i < 3; i++) { funcs[i] = tempFunc(i); } for (var j = 0; j < 3; j++) { funcs[j](); }
原始示例不起作用的原因是您在循環中創建的所有閉包都引用了相同的幀。實際上,在一個對像上只有一個i
變量有3個方法。他們都打印出相同的價值。
在閱讀了各種解決方案之後,我想補充一點,這些解決方案的工作原理是依賴範圍鏈的概念。這是JavaScript在執行期間解析變量的方式。
- 每個函數定義形成一個範圍,該範圍由聲明的所有局部變量
var
及其組成arguments
。 - 如果我們在另一個(外部)函數中定義了內部函數,則它形成一個鏈,並將在執行期間使用
- 執行函數時,運行時通過搜索範圍鏈來評估變量。如果變量可以在鏈的某個點找到,它將停止搜索並使用它,否則它將一直持續到達到屬於的全局範圍
window
。
在初始代碼中:
funcs = {};
for (var i = 0; i < 3; i++) {
funcs[i] = function inner() { // function inner's scope contains nothing
console.log("My value: " + i);
};
}
console.log(window.i) // test value 'i', print 3
當funcs
執行時,範圍鏈將是function inner -> global
。由於i
無法找到變量function inner
(既沒有聲明使用var
也沒有作為參數傳遞),它繼續搜索,直到i
最終在全局範圍中找到值window.i
。
通過將它包裝在外部函數中,可以顯式定義像share那樣的輔助函數,也可以使用像share那樣的匿名函數:
funcs = {};
function outer(i) { // function outer's scope contains 'i'
return function inner() { // function inner, closure created
console.log("My value: " + i);
};
}
for (var i = 0; i < 3; i++) {
funcs[i] = outer(i);
}
console.log(window.i) // print 3 still
當funcs
執行時,現在範圍鏈將是function inner -> function outer
。這個時間i
可以在外部函數的範圍中找到,它在for循環中執行3次,每次都i
正確綁定值。它不會使用window.i
內部執行時的值。
更多細節可以在here 找到
它包括我們在這裡創建閉包的常見錯誤,以及為什麼我們需要關閉和性能考慮。
我很驚訝沒有人建議使用該forEach
函數來更好地避免(重新)使用局部變量。事實上,for(var i ...)
由於這個原因,我根本就不再使用了。
[0,2,3].forEach(function(i){ console.log('My value:', i); });
// My value: 0
// My value: 2
// My value: 3
//編輯使用forEach
而不是地圖。
我更喜歡使用forEach
function,它有自己的閉包創建一個偽範圍:
var funcs = [];
new Array(3).fill(0).forEach(function (_, i) { // creating a range
funcs[i] = function() {
// now i is safely incapsulated
console.log("My value: " + i);
};
});
for (var j = 0; j < 3; j++) {
funcs[j](); // 0, 1, 2
}
這看起來比其他語言的範圍更醜,但恕我直言比其他解決方案更怪異。
這個問題確實展示了JavaScript的歷史!現在我們可以避免使用箭頭函數進行塊作用域,並使用Object方法直接從DOM節點處理循環。
const funcs = [1, 2, 3].map(i => () => console.log(i));
funcs.map(fn => fn())
const buttons = document.getElementsByTagName("button");
Object
.keys(buttons)
.map(i => buttons[i].addEventListener('click', () => console.log(i)));
<button>0</button><br>
<button>1</button><br>
<button>2</button>
還有另一個解決方案:不是創建另一個循環,而是綁定this
到return函數。
var funcs = [];
function createFunc(i) {
return function() {
console.log('My value: ' + i); //log value of i.
}.call(this);
}
for (var i = 1; i <= 5; i++) { //5 functions
funcs[i] = createFunc(i); // call createFunc() i=5 times
}
通過綁定它,也解決了這個問題。