JavaScript 循环内的闭包——简单实用的例子

2024-11-02 21:00:00
admin
原创
35
摘要:问题描述:var funcs = []; // let's create 3 functions for (var i = 0; i < 3; i++) { // and store them in funcs funcs[i] = function() { /...

问题描述:

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]();
}

运行代码片段Hide results展开片段

它输出的是:

我的价值: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>

运行代码片段Hide results展开片段

…或者异步代码,例如使用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));
}

运行代码片段Hide results展开片段

for in在和循环中也很明显for 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();
}

运行代码片段Hide results展开片段

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


解决方案 1:

嗯,问题在于i每个匿名函数内的变量都绑定到函数外部的相同变量。

ES6解决方案:let

ECMAScript 6 (ES6) 引入了 newletconst关键字,它们的作用域与var基于 - 的变量不同。例如,在具有let基于 - 的索引的循环中,循环的每次迭代都会有一个i具有循环作用域的新变量,因此您的代码将按预期工作。有很多资源,但我推荐2ality 的块作用域帖子,它是一个很好的信息来源。

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

但请注意,IE9-IE11 和 Edge 14 之前的 Edge 支持let但会出错(它们不会i每次都创建一个新的,因此上述所有函数都会像我们使用一样记录 3 var)。Edge 14 终于做对了。


ES5.1解决方案:forEach

由于该函数的可用性相对较高Array.prototype.forEach(2015 年),值得注意的是,在主要涉及对值数组进行迭代的情况下,.forEach()它提供了一种干净、自然的方式来为每次迭代获取不同的闭包。也就是说,假设您有某种包含值(DOM 引用、对象等)的数组,并且出现了为每个元素设置特定回调的问题,您可以这样做:

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

这个想法是,循环中使用的回调函数的每次调用都.forEach将是其自己的闭包。传递给该处理程序的参数是特定于迭代的特定步骤的数组元素。如果它在异步回调中使用,它不会与在迭代的其他步骤中建立的任何其他回调发生冲突。

如果您恰好使用 jQuery,该$.each()函数可以为您提供类似的功能。


经典解决方案:闭包

您要做的是将每个函数内的变量绑定到函数外部的单独的、不变的值:

var funcs = [];

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

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

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

运行代码片段Hide results展开片段

由于 JavaScript 中没有块作用域 - 只有函数作用域 - 通过将函数创建包装在新函数中,您可以确保“i”的值保持如您所愿。

解决方案 2:

尝试:

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]();
}

运行代码片段Hide results展开片段

编辑(2014):

我个人认为 @Aust关于使用 的最新回答.bind是目前做这种事情的最佳方式。_.partial当您不需要或不想弄乱bind's时,还有短划线/下划线thisArg

解决方案 3:

另一种尚未提及的方法是使用Function.prototype.bind

var funcs = {};
for (var i = 0; i < 3; i++) {
  funcs[i] = function(x) {
    console.log('My value: ' + x);
  }.bind(this, i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

运行代码片段Hide results展开片段

更新

正如@squint 和@mekdev 指出的那样,通过先在循环外创建函数,然后在循环内绑定结果,可以获得更好的性能。

function log(x) {
  console.log('My value: ' + x);
}

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = log.bind(this, i);
}

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

运行代码片段Hide results展开片段

解决方案 4:

使用立即调用函数表达式,这是包含索引变量的最简单且最易读的方式:

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);

}

运行代码片段Hide results展开片段

这会将迭代器发送i到我们定义为的匿名函数中index。这会创建一个闭包,变量i将被保存,以供稍后在 IIFE 中的任何异步功能中使用。

解决方案 5:

有点晚了,但是我今天正在探索这个问题,并注意到许多答案并没有完全解决 Javascript 如何处理范围的问题,而这基本上就是这个问题的关键。

因此,正如许多其他人提到的那样,问题在于内部函数引用了同一个i变量。那么,为什么我们不在每次迭代中创建一个新的局部变量,然后让内部函数引用该变量呢?

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    var ilocal = i; //create a new local variable
    funcs[i] = function() {
        console.log("My value: " + ilocal); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

运行代码片段Hide results展开片段

就像之前一样,每个内部函数输出分配给 的最后一个值i,现在每个内部函数只输出分配给 的最后一个值ilocal。但是每次迭代不应该都有自己的 吗ilocal

事实证明,这就是问题所在。每次迭代都共享相同的范围,因此第一次迭代之后的每次迭代都会覆盖ilocal。摘自MDN:

重要提示:JavaScript 没有块作用域。通过块引入的变量的作用域为包含函数或脚本,并且设置它们的效果会超出块本身。换句话说,块语句不会引入作用域。虽然“独立”块是有效的语法,但您不想在 JavaScript 中使用独立块,因为它们不会按照您的想象执行操作,如果您认为它们可以像 C 或 Java 中的块一样执行操作的话。

重申强调:

JavaScript 没有块作用域。通过块引入的变量的作用域为包含该块的函数或脚本

ilocal我们可以在每次迭代中声明它之前通过检查来看到这一点:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
  console.log(ilocal);
  var ilocal = i;
}

运行代码片段Hide results展开片段

这正是这个错误如此棘手的原因。即使你重新声明了一个变量,Javascript 也不会抛出错误,JSLint 甚至不会抛出警告。这也是解决这个问题的最佳方法是利用闭包的原因,闭包的本质是 Javascript 中的内部函数可以访问外部变量,因为内部作用域“封闭”了外部作用域。

闭包

这也意味着内部函数“保留”外部变量并保持其活动状态,即使外部函数返回也是如此。为了利用这一点,我们创建并调用一个包装函数,纯粹是为了创建一个新作用域,ilocal在新作用域中声明,并返回一个使用的内部函数ilocal(更多解释见下文):

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = (function() { //create a new scope using a wrapper function
        var ilocal = i; //capture i into a local var
        return function() { //return the inner function
            console.log("My value: " + ilocal);
        };
    })(); //remember to run the wrapper function
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

运行代码片段Hide results展开片段

在包装函数内部创建内部函数会为内部函数提供一个只有它自己才能访问的私有环境,即“闭包”。因此,每次调用包装函数时,我们都会创建一个具有自己独立环境的新内部函数,确保ilocal变量不会发生冲突和相互覆盖。经过一些小的优化,许多其他 SO 用户给出了最终答案:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = wrapper(i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}
//creates a separate environment for the inner function
function wrapper(ilocal) {
    return function() { //return the inner function
        console.log("My value: " + ilocal);
    };
}

运行代码片段Hide results展开片段

更新

随着 ES6 成为主流,我们现在可以使用 newlet关键字来创建块范围变量:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (let i = 0; i < 3; i++) { // use "let" to declare "i"
    funcs[i] = function() {
        console.log("My value: " + i); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) { // we can use "var" here without issue
    funcs[j]();
}

运行代码片段Hide results展开片段

看看现在有多简单!有关更多信息,请参阅此答案,我的信息基于此答案。

解决方案 6:

随着 ES6 得到广泛支持,这个问题的最佳答案已经改变。ES6 为这种情况提供了letconst关键字。我们不用再使用闭包了,只需使用let来设置循环范围变量,如下所示:

var funcs = [];

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

运行代码片段Hide results展开片段

val然后会指向特定于循环的特定回合的对象,并返回正确的值,而无需额外的闭包符号。这显然大大简化了这个问题。

const类似于,let但有额外的限制,即变量名在初始分配后不能重新绑定到新的引用。

浏览器支持现已面向最新版本的浏览器。/目前在最新的constFirefox let、Safari、Edge 和 Chrome 中受支持。它也受 Node 支持,您可以利用 Babel 等构建工具在任何地方使用它。您可以在此处查看一个工作示例: http: //jsfiddle.net/ben336/rbU4t/2/

文档在这里:

  • 常量

但请注意,IE9-IE11 和 Edge 14 之前的 Edge 支持let但会出错(它们不会i每次都创建一个新的,因此上述所有函数都会像我们使用一样记录 3 var)。Edge 14 终于做对了。

解决方案 7:

另一种说法是,i函数中的是在执行函数时绑定的,而不是在创建函数时绑定的。

创建闭包时,i是指向外部作用域中定义的变量的引用,而不是创建闭包时对该变量的副本。它将在执行时进行评估。

大多数其他答案都提供了解决方法,即创建另一个不会改变值的变量。

我只是想添加一个解释以便更清楚。对于解决方案,就我个人而言,我会选择 Harto 的解决方案,因为从这里的答案来看,这是最不言自明的方法。发布的任何代码都可以工作,但我会选择一个闭包工厂,而不是写一堆注释来解释为什么我要声明一个新变量(Freddy 和 1800's)或有奇怪的嵌入式闭包语法(apphacker)。

解决方案 8:

您需要了解的是,JavaScript 中变量的作用域基于函数。这与 C# 中的一个重要区别在于,C# 具有块作用域,只需将变量复制到 for 中的块作用域即可。

将其包装在一个评估返回函数的函数中(如 apphacker 的答案)就可以了,因为变量现在具有函数范围。

还有一个 let 关键字可以代替 var,这样就可以使用块作用域规则。在这种情况下,在 for 中定义一个变量就可以了。话虽如此,由于兼容性问题,let 关键字并不是一个实用的解决方案。

var funcs = {};

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

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

运行代码片段Hide results展开片段

解决方案 9:

下面是该技术的另一种变体,类似于 Bjorn(apphacker)的技术,它允许您在函数内部分配变量值,而不是将其作为参数传递,这有时可能更清晰:

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

运行代码片段Hide results展开片段

请注意,无论您使用哪种技术,index变量都会变成一种静态变量,绑定到内部函数的返回副本。也就是说,对其值的更改在调用之间会保留。这非常方便。

解决方案 10:

这描述了在 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
还会创建一个新的副本来引用新对象。因此,counter1counter2彼此独立。

循环中的闭包

在循环中使用闭包比较棘手。

考虑:

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

这是有效的,因为函数范围内的局部变量以及函数参数变量在进入时会直接分配新的副本。

解决方案 11:

最简单的解决办法是,

而不是使用:

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

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

它会连续 3 次提示“2”。这是因为在 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其作为参数传递并捕获为i。由于匿名函数是立即执行的,因此i匿名函数内部定义的每个函数的值都是不同的。

此解决方案似乎适用于任何此类问题,因为它只需要对受此问题困扰的原始代码进行最小程度的更改。事实上,这是设计使然,这根本不应该成为问题!

解决方案 12:

这是一个简单的解决方案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
}

运行代码片段Hide results展开片段

印刷:

My value: 0
My value: 1
My value: 2

解决方案 13:

试试这个较短的

  • 没有数组

  • 没有额外的 for 循环

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

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

http://jsfiddle.net/7P6EN/

解决方案 14:

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++就会达到,这会导致条件失败并结束循环。此时是,因此当使用 和进行评估时,它每次都是 3。i`3i < 3i3funcs[someIndex]()`i

要解决这个问题,您必须i在遇到时进行评估。请注意,这已经以(其中有 3 个唯一索引)的形式发生了funcs[i]。有几种方法可以捕获此值。一种方法是将其作为参数传递给函数,这里已经以几种方式显示了这一点。

另一个选择是构造一个能够覆盖变量的函数对象。这可以通过以下方式实现

jsFiddle Demo

funcs[i] = new function() {   
    var closedVariable = i;
    return function(){
        console.log("My value: " + closedVariable); 
    };
};

解决方案 15:

JavaScript 函数在声明时“关闭”它们可以访问的范围,并且即使该范围内的变量发生变化,仍能保留对该范围的访问权限。

var funcs = []

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

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

运行代码片段Hide results展开片段

上面数组中的每个函数都覆盖全局范围(全局,只是因为这恰好是它们被声明的范围)。

稍后调用这些函数,在全局范围内记录最新的值i。这就是闭包的魔力所在,也是它的挫败之处。

“JavaScript 函数封闭其声明的范围,并且即使该范围内的变量值发生变化,仍保留对该范围的访问权限。”

使用letInstead ofvar可以解决这个问题,方法是每次for循环运行时创建一个新的作用域,为每个函数创建一个单独的作用域以进行封闭。其他各种技术对额外的函数执行相同的操作。

var funcs = []

for (let i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    console.log(i)
  }
}

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

运行代码片段Hide results展开片段

let使变量具有块作用域。块用花括号表示,但在 for 循环的情况下,初始化变量(i在我们的例子中)被认为是在括号中声明的。)

解决方案 16:

在阅读了各种解决方案之后,我想补充一点,这些解决方案之所以有效,是因为它们依赖于作用域链的概念。这是 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 innervari`window.i

通过将其包装在外部函数中,可以像harto那样明确定义辅助函数,或者像Bjorn那样使用匿名函数:

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

funcsgets 执行时,作用域链将变为function inner -> function outer。此时i可以在 outer 函数的作用域中找到,它在 for 循环中被执行了 3 次,每次都有i正确绑定的值。它不会window.i在 inner 执行时使用 的值。

更多细节可以在这里找到

,它包括在循环中创建闭包的常见错误,以及我们为什么需要闭包和性能考虑。

解决方案 17:

通过 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

解决方案 18:

我们将逐一检查您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 that one 的属性。您将看到一个名为,expand that one 的数组对象。您将发现在对象中声明了一个值为 3 的属性。[[Scopes]]`"Global"`'i'

在此处输入图片描述

在此处输入图片描述

结论:

  1. 'var'当您在函数外部声明一个变量时,它将成为全局变量(您可以通过在控制台窗口中键入i
    来检查window.i。它将返回 3)。

  2. 您声明的匿名函数将不会调用并检查函数内部的值,除非您调用该函数。

  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

解决方案 19:

我很惊讶还没有人建议使用该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代替地图。

解决方案 20:

您的原始示例不起作用的原因是您在循环中创建的所有闭包都引用了同一框架。实际上,一个对象上只有一个i变量,却有 3 个方法。它们都打印出相同的值。

解决方案 21:

这个问题确实展示了 JavaScript 的历史!现在我们可以使用箭头函数避免块作用域,并使用 Object 方法直接从 DOM 节点处理循环。

const funcs = [1, 2, 3].map(i => () => console.log(i));
funcs.map(fn => fn())

运行代码片段Hide results展开片段

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>

运行代码片段Hide results展开片段

解决方案 22:

首先,了解一下此代码有什么问题:

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。

现在为了解决这个问题,我们有很多选择。以下是其中两个:

  1. 我们可以i用初始化,或者用let初始化一个新变量并使其等于。因此,在进行调用时,将被使用,并且其范围将在初始化后结束。对于调用,将再次初始化:index`letiindex`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]();                        
}
  1. 其他选项可以引入一个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]();                        
}

解决方案 23:

使用闭包结构,这将减少额外的 for 循环。您可以在单个 for 循环中完成此操作:

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

解决方案 24:

直到 ES5,这个问题只能使用闭包来解决。

但现在在ES6 中,我们有块级作用域变量。在第一个for 循环中将var改为let即可解决问题。

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 (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

运行代码片段Hide results展开片段

解决方案 25:

如果您遇到与循环有关的此类问题while,而不是for循环,例如:

var i = 0;
while (i < 5) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
  i++;
}

运行代码片段Hide results展开片段

关闭当前值的技术有点不同。在块const内声明一个块范围的变量while,并将当前值分配i给它。然后,无论变量在哪里异步使用,i都用新的块范围变量替换:

var i = 0;
while (i < 5) {
  const thisIterationI = i;
  setTimeout(function() {
    console.log(thisIterationI);
  }, i * 1000);
  i++;
}

运行代码片段Hide results展开片段

对于不支持块范围变量的老版本浏览器,你可以使用 IIFE 调用i

var i = 0;
while (i < 5) {
  (function(innerI) {
    setTimeout(function() {
      console.log(innerI);
    }, innerI * 1000);
  })(i);
  i++;
}

运行代码片段Hide results展开片段

如果要调用的异步操作恰好像setTimeout上面的一样,那么您还可以setTimeout使用第三个参数来指示调用传递的函数的参数:

var i = 0;
while (i < 5) {
  setTimeout(
    (thisIterationI) => { // Callback
      console.log(thisIterationI);
    },
    i * 1000, // Delay
    i // Gets passed to the callback; becomes thisIterationI
  );
  i++;
}

运行代码片段Hide results展开片段

解决方案 26:

您可以使用声明式模块来列出数据,例如query-js (*)。在这种情况下,我个人认为声明式方法并不那么令人惊讶

var funcs = Query.range(0,3).each(function(i){
     return  function() {
        console.log("My value: " + i);
    };
});

然后您可以使用第二个循环并获得预期的结果,或者您可以这样做

funcs.iterate(function(f){ f(); });

(*) 我是 query-js 的作者,因此倾向于使用它,所以不要把我的话当作只针对声明式方法的该库的推荐:)

解决方案 27:

我更喜欢使用forEach函数,它有自己的闭包来创建伪范围:

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
}

这看起来比其他语言中的范围更丑陋,但在我看来,比其他解决方案更不可怕。

解决方案 28:

还有另一个解决方案:不需要创建另一个循环,只需将其绑定this到返回函数。

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
}

运行代码片段Hide results展开片段

通过绑定这个,也解决了这个问题。

解决方案 29:

使用 let(blocked-scope) 而不是 var。

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

运行代码片段Hide results展开片段

解决方案 30:

您的代码不起作用,因为它的作用是:

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

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

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用