JavaScript循环内的闭包 - 简单实用示例

3229

var funcs = [];
// let's create 3 functions
for (var i = 0; i < 3; i++) {
  // and store them in funcs
  funcs[i] = function() {
    // each should log its value.
    console.log("My value:", i);
  };
}
for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

它会输出以下内容:

我的值:3
我的值:3
我的值:3

但我想要输出:

我的值:0
我的值:1
我的值:2


当函数的延迟由事件侦听器引起时,就会出现同样的问题:

var buttons = document.getElementsByTagName("button");
// let's create 3 functions
for (var i = 0; i < buttons.length; i++) {
  // as event listeners
  buttons[i].addEventListener("click", function() {
    // each should log its value.
    console.log("My value:", i);
  });
}
<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++) {
  // Log `i` as soon as each promise resolves.
  wait(i * 100).then(() => console.log(i));
}

这也可以在 for infor of 循环中看出:

const arr = [1,2,3];
const fns = [];

for (var i in arr){
  fns.push(() => console.log("index:", i));
}

for (var v of arr){
  fns.push(() => console.log("value:", v));
}

for (const n of arr) {
  var obj = { number: n }; // or new MyLibObject({ ... })
  fns.push(() => console.log("n:", n, "|", "obj:", JSON.stringify(obj)));
}

for(var f of fns){
  f();
}

这个基本问题有什么解决方案?


59
在ES6中,一个简单的解决方案是使用let声明变量_i,它具有循环体作用域。 - Tomas Nikodym
4
JS函数在声明时“闭合”了它们可以访问的作用域,并保留对该作用域的访问权限,即使该作用域中的变量发生变化。上述数组中的每个函数都关闭了全局作用域(全局作用域只是因为它们恰好是在其中声明的作用域)。稍后,这些函数被调用以记录全局作用域中“i”的最新值。这就是JS :) 使用let代替var通过在每次循环运行时创建一个新的作用域,为每个函数创建单独的作用域来解决此问题。其他各种技术使用额外的函数完成相同的操作。 - Costa Michailidis
45个回答

3
你的代码不能正常运行,因为它所做的是:
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的值保存到函数中,然后再将函数保存到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的值。

这只是解决此问题的多种方法之一。


2

只需将变量关键字 var 改为 let。

var 的作用域是函数级别的。

let 的作用域是块级别的。

当您开始编写代码时,for循环将迭代并将i的值分配为3,这个值将在您的代码中保持为3。我建议您阅读更多有关 node 中作用域的内容(例如 let、var、const 等)。

funcs = [];
for (let i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] =async function() {          // and store them in funcs
    await 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
}

2

计数器作为基本类型

让我们将回调函数定义如下:

// ****************************
// COUNTER BEING A PRIMITIVE
// ****************************
function test1() {
    for (var i=0; i<2; i++) {
        setTimeout(function() {
            console.log(i);
        });
    }
}
test1();
// 2
// 2

完成超时后,它将为两者都打印2。这是因为回调函数根据定义函数的词法作用域访问该值。
为了在定义回调时传递和保留该值,我们可以创建一个闭包,以在调用回调之前保留该值。可以按以下方式完成:
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

2
这是异步代码经常遇到的问题,变量i是可变的,当函数调用时,使用i的代码将被执行,并且i将变为其最后一个值,这意味着在循环内创建的所有函数都将创建一个闭包,并且i将等于3(for循环的上限+1)。
解决方法是创建一个函数,它将保存每次迭代的i的值,并强制复制i(因为它是原始类型,如果有帮助的话,请将其视为快照)。

2

只是将var替换为let

var funcs = []
// change `var` to `let`
for (let i = 0; i < 3; i++) {
  funcs[i] = function(){
    console.log("My value:", i); //change to the copy
  }
}
for (var j = 0; j < 3; j++) {
  funcs[j]()
}


2

许多解决方案似乎是正确的,但它们没有提到这被称为柯里化,这是一种用于像这样的情况的函数式编程设计模式。在浏览器中,比绑定快3-10倍。

var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = curryShowValue(i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

function curryShowValue(i) {
  return function showValue() {
    console.log("My value: " + i);
  }
}

请查看不同浏览器中的性能提升


@TinyGiant 函数返回的示例仍然是为性能而优化的柯里化。我不会像所有 JavaScript 博客作者一样跳上箭头函数的车,它们看起来很酷、很干净,但是促使我们在内联编写函数而不是使用预定义函数,这可能是一个不明显的陷阱。另一个问题是它们不仅仅是语法糖,因为它们执行了不必要的绑定,从而创建了包装闭包。 - Pawel
2
未来读者警告:此答案错误地应用了“柯里化”这个术语。"柯里化是指将接受多个参数的函数分解为一系列接受部分参数的函数。"(https://dev59.com/dXVD5IYBdhLWcg3wQZUg#36321)。此代码完全没有做到这一点。在这里,你所做的仅仅是将已被接受的答案的代码移动一些位置,改变一些风格和命名,然后称其为柯里化,这明显是错误的。 - user4639281

0

虽然这个问题已经很老并且得到了回答,但我有另一个相当有趣的解决方案:

var funcs = [];

for (var i = 0; i < 3; i++) {     
  funcs[i] = function() {          
    console.log("My value: " + i); 
 };
}

for (var i = 0; i < 3; i++) {
  funcs[i]();
}

这个改变非常微小,几乎难以看出我做了什么。我将第二个迭代器从j改为i。这样可以在时间上刷新i的状态,从而给您提供所需的结果。我是无意中这样做的,但考虑到之前的答案,这是有道理的。

我写这篇文章是为了指出这个小差异,但却非常重要。希望这能帮助其他像我一样的学习者消除一些困惑。

注意:我分享这个并不是因为我认为这是正确的答案。这是一个不太可靠的解决方案,在某些情况下可能会出现问题。实际上,我很惊讶它真的有效。


它之所以能够工作,是因为在第二个循环中,你正在覆盖函数引用的同一个 i。请考虑,在整个代码片段中只有一个 i 变量。这相当于:i = 0; funcs[0](); i = 1; funcs[1](); .. - nickf
正确,考虑到作用域的其他答案,这是有道理的,但仍然有点违反直觉。 - Brooks DuBois
你正在将值i3覆盖为0,1,2,3,并立即使用这些值进行调用。@nickf的意思是j=0也会变成funcs[0]吗? - Ashish Kamble

0

好的。我已经阅读了所有答案,尽管这里有一个很好的解释 - 但我就是无法让它起作用。所以我去网上找了一下。https://dzone.com/articles/why-does-javascript-loop-only-use-last-value这个人给出了一个这里没有介绍的答案。所以我想发一个简短的例子。这对我来说更有意义。

长话短说,LET命令很好,但现在才开始使用。但是,LET命令实际上只是一个TRY-CATCH组合。这可以一直使用到IE3(我相信)。使用TRY-CATCH组合 - 生活就简单而美好。可能这也是EMCScript人决定使用它的原因。它也不需要setTimeout()函数。所以不浪费时间。基本上,你需要为每个FOR循环提供一个TRY-CATCH组合。这里是一个例子:

    for( var i in myArray ){
       try{ throw i }
       catch(ii){
// Do whatever it is you want to do with ii
          }
       }

如果您有多个FOR循环,只需为每个循环放置一个TRY-CATCH组合即可。此外,我个人总是使用所使用的FOR变量的双字母。因此,“i”的双字母是“ii”,依此类推。我正在使用这种技术在例程中发送鼠标悬停命令到不同的例程。

0

这证明了JavaScript在'闭包'和'非闭包'工作方式方面有多么丑陋。

就以下情况而言:

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.
  };
}

funcs[i]是一个全局函数,而'console.log("My value: " + i);'正在打印全局变量i。

在这种情况下

var funcs = [];

function createfunc(i) {
    return function() { console.log("My value: " + i); };
}

for (var i = 0; i < 3; i++) {
    funcs[i] = createfunc(i);
}

由于 JavaScript 的这种扭曲的闭包设计,'console.log("My value: " + i);' 打印的是外部函数 'createfunc(i)' 中的 i。
这一切都是因为 JavaScript 无法像 C 编程语言那样设计出像函数内的 'static' 变量这样体面的东西!

0

假设您不使用ES6; 您可以使用IIFEs:

var funcs = [];
for (var i = 0; i < 13; i++) {      
    funcs[i] = (function(x) {
      console.log("My value: " + i)
     })(i);
   }

但这会有所不同。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接