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

问:

变量函数 = []; // 让我们创建 3 个函数 for (var i = 0; i < 3; i++) { // 并将它们存储在 funcs funcs[i] = function() { // 每个都应该记录它的值。 console.log(“我的值:” + i); }; } for (var j = 0; j < 3; j++) { // 现在让我们运行每一个来查看 funcsj; }

它输出这个:

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

而我希望它输出:

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

当函数运行延迟是由使用事件监听器引起时,也会出现同样的问题:

var 按钮 = document.getElementsByTagName(“button”); // 让我们创建 3 个函数 for (var i = 0; i < buttons.length; i++) { // 作为事件监听器 button[i].addEventListener(“click”, function() { // 每个都应该记录它的值。 console.log(“我的值:” + i); }); } 0 1 2

… 或异步代码,例如使用 Promises:

// 一些异步等待函数 const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms)); for (var i = 0; i < 3; i++) { // 每个 Promise 解决后立即记录 i。等待(i * 100).then(() => console.log(i)); }

在 for in 和 for of 循环中也很明显:

常量 arr = [1,2,3];常量 fns = []; for(var i in arr){ fns.push(() => console.log(index: ${i})); } for(var v of arr){ fns.push(() => console.log(value: ${v})); } for(var f of fns){ f(); }

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

答1:

huntsbot.com提供全网独家一站式外包任务、远程工作、创意产品分享与订阅服务!

好吧,问题是每个匿名函数中的变量 i 都绑定到函数外部的同一个变量。

ES6 解决方案:让

ECMAScript 6 (ES6) 引入了新的 let 和 const 关键字,它们的作用域不同于基于 var 的变量。例如,在具有基于 let 的索引的循环中,循环中的每次迭代都会有一个具有循环范围的新变量 i,因此您的代码将按预期工作。有很多资源,但我建议将 2ality’s block-scoping post 作为重要的信息来源。

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

但请注意,Edge 14 之前的 IE9-IE11 和 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() 函数可以为您提供类似的功能。

经典解决方案:闭包

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

变量函数 = []; function createfunc(i) { return function() { console.log(“我的值:” + i); }; } for (var i = 0; i < 3; i++) { funcs[i] = createfunc(i); } for (var j = 0; j < 3; j++) { // 现在让我们运行每一个来查看 funcsj; }

由于 JavaScript 中没有块作用域——只有函数作用域——通过将函数创建包装在一个新函数中,您可以确保“i”的值保持您的预期。

function createfunc(i) { return function() { console.log("My value: " + i); }; } 不是因为使用了变量 i 而仍然是闭包吗?

不幸的是,这个答案已经过时,没有人会在底部看到正确的答案 - 现在使用 Function.bind() 绝对是更可取的,请参阅 stackoverflow.com/a/19323214/785541。

@Wladimir:您关于 .bind() 是 “正确答案” 的建议是不正确的。他们每个人都有自己的位置。如果不绑定 this 值,则无法使用 .bind() 绑定参数。此外,您还获得了 i 参数的副本,但无法在调用之间对其进行变异,这有时是必需的。所以它们是完全不同的构造,更不用说 .bind() 的实现在历史上一直很慢。当然,在这个简单的例子中,两者都可以,但是闭包是一个需要理解的重要概念,这就是问题所在。

请停止使用这些 for-return 函数技巧,改用 [].forEach 或 [].map,因为它们避免重用相同的范围变量。

@ChristianLandgren:只有在迭代数组时才有用。这些技术不是“黑客”。它们是必不可少的知识。

答2:

与HuntsBot一起,探索全球自由职业机会–huntsbot.com

尝试:

变量函数 = []; for (var i = 0; i < 3; i++) { funcs[i] = (function(index) { return function() { console.log(“我的值:” + index); }; }(i)) ; } for (var j = 0; j < 3; j++) { funcsj; }

编辑(2014):

我个人认为@Aust 的 more recent answer about using .bind 是现在做这种事情的最佳方式。当您不需要或不想弄乱 bind 的 thisArg 时,还有 lo-dash/underscore 的 _.partial。

关于 }(i)); 的任何解释?

@aswzen 我认为它将 i 作为参数 index 传递给函数。

它实际上是在创建局部变量索引。

立即调用函数表达式,即 IIFE。 (i) 是立即调用的匿名函数表达式的参数,并且索引从 i 开始设置。

答3:

保持自己快人一步,享受全网独家提供的一站式外包任务、远程工作、创意产品订阅服务–huntsbot.com

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

变函数 = {}; for (var i = 0; i < 3; i++) { funcs[i] = function(x) { console.log(‘我的值:’ + x); }.bind(this, i); } for (var j = 0; j < 3; j++) { funcsj; }

更新

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

function log(x) { console.log(‘我的值:’ + x); } 变量函数 = []; for (var i = 0; i < 3; i++) { funcs[i] = log.bind(this, i); } for (var j = 0; j < 3; j++) { funcsj; }

这也是我这几天在做的事情,我也喜欢 lo-dash/underscore 的 _.partial

.bind() 将在很大程度上被 ECMAScript 6 功能淘汰。此外,这实际上每次迭代都会创建两个函数。首先是匿名的,然后是由 .bind() 生成的。更好的用法是在循环外部创建它,然后在内部创建它.bind()。

@squint @mekdev - 你们都是正确的。我的初始示例是快速编写的,以演示如何使用 bind。根据您的建议,我添加了另一个示例。

我认为与其在两个 O(n) 循环上浪费计算,不如执行 for (var i = 0; i < 3; i++) { log.call(this, i); }

.bind() 执行公认的答案建议 PLUS 摆弄 this。

答4:

huntsbot.com – 程序员副业首选,一站式外包任务、远程工作、创意产品分享订阅平台。

使用 Immediately-Invoked Function Expression,最简单且最易读的方式来包含索引变量:

for (var i = 0; i < 3; i++) { (function(index) { console.log('iterator: ’ + index); //现在你也可以在这里循环一个 ajax 调用 //不会丢失迭代器值:$.ajax({}); })(i); }

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

为了进一步提高代码可读性并避免混淆 i 是什么,我将函数参数重命名为 index。

您将如何使用这种技术来定义原始问题中描述的数组函数?

@Nico 与原始问题中显示的方式相同,只是您将使用 index 而不是 i。

@JLRishe var funcs = {}; for (var i = 0; i < 3; i++) { funcs[i] = (function(index) { return function() {console.log('iterator: ' + index);}; })(i); }; for (var j = 0; j < 3; j++) { funcs[j](); }

@Nico 在 OP 的特殊情况下,他们只是在迭代数字,所以这对于 .forEach() 来说不是一个很好的情况,但是很多时候,当一个人从一个数组开始时,forEach() 是一个不错的选择,例如:var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });

答5:

HuntsBot周刊–不定时分享成功产品案例,学习他们如何成功建立自己的副业–huntsbot.com

聚会有点晚了,但我今天正在探索这个问题,并注意到许多答案并没有完全解决 Javascript 如何处理作用域,这本质上就是归结为什么。

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

//覆盖console.log()这样你就可以看到控制台输出console.log = function(msg) {document.body.innerHTML += ‘’ + msg + ‘’;};变函数 = {}; for (var i = 0; i < 3; i++) { var ilocal = i; //创建一个新的局部变量 funcs[i] = function() { console.log(“我的值:” + ilocal); //每个都应该引用自己的局部变量 }; } for (var j = 0; j < 3; j++) { funcsj; }

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

原来,这就是问题所在。每次迭代都共享相同的范围,因此第一次之后的每次迭代都只是覆盖 ilocal。从 MDN:

重要提示:JavaScript 没有块作用域。与块一起引入的变量的作用域是包含函数或脚本,并且设置它们的效果会持续到块本身之外。换句话说,块语句不引入范围。尽管“独立”块是有效的语法,但您不想在 JavaScript 中使用独立块,因为如果您认为它们在 C 或 Java 中执行类似块的操作,它们不会按照您的想法执行。

再次强调:

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

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

//覆盖console.log()这样你就可以看到控制台输出console.log = function(msg) {document.body.innerHTML += ‘’ + msg + ‘’;};变函数 = {}; for (var i = 0; i < 3; i++) { console.log(ilocal);变量 ilocal = i; }

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

https://i.stack.imgur.com/60fH9.png

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

//覆盖console.log()这样你就可以看到控制台输出console.log = function(msg) {document.body.innerHTML += ‘’ + msg + ‘’;};变函数 = {}; for (var i = 0; i < 3; i++) { funcs[i] = (function() { //使用包装函数创建新作用域 var ilocal = i; //将 i 捕获到本地 var return function( ) { //返回内部函数 console.log(“我的值:” + ilocal); }; })(); //记得运行包装函数 } for (var j = 0; j < 3; j++) { funcsj; }

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

//覆盖console.log()这样你就可以看到控制台输出console.log = function(msg) {document.body.innerHTML += ‘’ + msg + ‘’;};变函数 = {}; for (var i = 0; i < 3; i++) { funcs[i] = wrapper(i); } for (var j = 0; j < 3; j++) { funcsj; } //为内部函数创建一个单独的环境 function wrapper(ilocal) { return function() { //返回内部函数 console.log(“My value:” + ilocal); }; }

更新

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

//覆盖console.log()这样你就可以看到控制台输出console.log = function(msg) {document.body.innerHTML += ‘’ + msg + ‘’;};变函数 = {}; for (let i = 0; i < 3; i++) { // 使用 “let” 声明 “i” funcs[i] = function() { console.log("My value: " + i); //每个都应该引用自己的局部变量 }; } for (var j = 0; j < 3; j++) { // 我们可以在这里使用 “var” 而不会出现问题 funcsj; }

看看现在多么容易!有关详细信息,请参阅我的信息所依据的 this answer。

现在,在 JavaScript 中使用 let 和 const 关键字进行了块作用域。如果将这个答案扩大到包括它,我认为它将在全球范围内更加有用。

@TinyGiant 确定,我添加了一些关于 let 的信息并链接了更完整的解释

@woojoo666 您的答案是否也适用于像这样在循环中调用两个交替 URL:i=0; while(i < 100) { setTimeout(function(){ window.open("https://www.bbc.com","_self") }, 3000); setTimeout(function(){ window.open("https://www.cnn.com","_self") }, 3000); i++ }? (可以用 getelementbyid 替换 window.open() ......)

@nuttyaboutnatty 对这么晚的回复感到抱歉。您的示例中的代码似乎无法正常工作。您没有在超时函数中使用 i,因此您不需要闭包

哎呀,对不起,意思是说“您的示例中的代码似乎已经有效”

答6:

保持自己快人一步,享受全网独家提供的一站式外包任务、远程工作、创意产品订阅服务–huntsbot.com

随着 ES6 现在得到广泛支持,这个问题的最佳答案已经改变。 ES6 针对这种情况提供了 let 和 const 关键字。我们可以使用 let 设置循环范围变量,而不是搞乱闭包,如下所示:

变量函数 = []; for (let i = 0; i < 3; i++) { funcs[i] = function() { console.log(“我的值:” + i); }; }

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

const 与 let 类似,但附加限制是变量名在初始赋值后不能重新绑定到新引用。

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

这里的文档:

常量

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

不幸的是,仍然不完全支持“让”,尤其是在移动设备中。 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…

截至 16 年 6 月,所有主要浏览器版本都支持 let,除了 iOS Safari、Opera Mini 和 Safari 9。Evergreen 浏览器都支持它。 Babel 将正确地转换它以保持预期的行为,而无需打开高合规模式。

@DanPantry 是的,是时候更新了 :) 更新以更好地反映当前状态,包括添加对 const、文档链接和更好的兼容性信息的提及。

这不就是我们使用 babel 转译我们的代码以便不支持 ES6/7 的浏览器可以理解发生了什么的原因吗?

答7:

HuntsBot周刊–不定时分享成功产品案例,学习他们如何成功建立自己的副业–huntsbot.com

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

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

大多数其他答案提供了通过创建另一个不会为您更改值的变量来解决问题的方法。

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

答8:

huntsbot.com汇聚了国内外优秀的初创产品创意,可按收入、分类等筛选,希望这些产品与实践经验能给您带来灵感。

您需要了解的是javascript中变量的范围是基于函数的。这与说 c# 有块范围的重要区别,只需将变量复制到 for 中的一个即可。

将它包装在一个评估返回函数的函数中,就像 apphacker 的答案一样,因为变量现在具有函数范围。

还有一个 let 关键字代替 var,这将允许使用块范围规则。在这种情况下,在 for 中定义一个变量就可以了。也就是说,由于兼容性,let 关键字不是一个实用的解决方案。

变函数 = {}; for (var i = 0; i < 3; i++) { 让索引 = i; //添加这个 funcs[i] = function() { console.log(“我的值:” + index); //更改为副本 }; } for (var j = 0; j < 3; j++) { funcsj; }

@nickf 哪个浏览器?正如我所说,它存在兼容性问题,我的意思是严重的兼容性问题,就像我认为 IE 不支持 let 一样。

@nickf 是的,请检查此参考:developer.mozilla.org/En/New_in_JavaScript_1.7 ...检查 let 定义部分,循环中有一个 onclick 示例

@nickf 嗯,实际上您必须明确指定版本: ...由于 IE 限制,我实际上并没有在任何地方使用它,它只是不是实际的 :(

您可以在此处查看对不同版本的浏览器支持es.wikipedia.org/wiki/Javascript

另请参阅What browsers currently support javascript's 'let' keyword?

答9:

一个优秀的自由职业者,应该有对需求敏感和精准需求捕获的能力,而huntsbot.com提供了这个机会

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

变量函数 = []; for (var i = 0; i < 3; i++) { funcs[i] = (function() { var index = i; return function() { console.log(“我的值:” + index); } }) (); }

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

一个优秀的自由职业者,应该有对需求敏感和精准需求捕获的能力,而huntsbot.com提供了这个机会

谢谢,您的解决方案有效。但是我想问一下为什么这行得通,但是交换 var 行和 return 行行不通?谢谢!

@midnite 如果您交换 var 和 return 则在返回内部函数之前不会分配变量。

答10:

huntsbot.com汇聚了国内外优秀的初创产品创意,可按收入、分类等筛选,希望这些产品与实践经验能给您带来灵感。

这描述了在 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);
  }
}

这是因为函数作用域中的局部变量以及函数参数变量在进入时被分配了新的副本。

小说明:在循环中的闭包的第一个示例中,counters[0] 和 counters[1] 不是独立的,不是因为性能原因。原因是在执行任何代码之前评估 var obj = {counter: 0};,如下所述:MDN var:var 声明,无论它们出现在哪里,都会在执行任何代码之前进行处理。

答11:

huntsbot.com高效搞钱,一站式跟进超10+任务平台外包需求

最简单的解决方案是,

而不是使用:

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 作为参数传递并将其捕获为 i。由于匿名函数会立即执行,因此在匿名函数内部定义的每个函数的 i 值都是不同的。

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

在书中读过类似的东西一次。我也更喜欢这个,因为您不必(尽可能多地)触摸现有代码,并且一旦您了解了自调用函数模式,您这样做的原因就很明显了:在新创建的变量中捕获该变量范围。

@DanMan 谢谢。自调用匿名函数是处理 javascript 缺少块级变量范围的好方法。

自调用或自调用不是此技术的合适术语,IIFE(立即调用函数表达式)更准确。参考:benalman.com/news/2010/11/…

原文链接:https://www.huntsbot.com/qa/1j9g/javascript-closure-inside-loops-simple-practical-example?lang=zh_CN&from=csdn

HuntsBot周刊–不定时分享成功产品案例,学习他们如何成功建立自己的副业–huntsbot.com

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值