8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

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

Ferruccio 1月前

138 0

var funcs = [];// 让我们创建 3 个函数for (var i = 0; i < 3; i++) { // 并将它们存储在 funcs 中 funcs[i] = function() { // 每个都应该记录其值。console.log(\'My value:\', i)...

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

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

帖子版权声明 1、本帖标题:JavaScript 循环内的闭包——简单实用的例子
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由Ferruccio在本站《arrays》版块原创发布, 转载请注明出处!
最新回复 (0)
  • JS 函数在声明时“封闭”了它们可以访问的范围,并且即使该范围内的变量发生变化,仍可访问该范围。上面数组中的每个函数都封闭了全局范围(全局,只是因为这恰好是它们声明的范围)。稍后调用这些函数,记录

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

    ES6解决方案: let

    ECMAScript 6 (ES6) 引入了 new let const 关键字,它们的作用域与基于 - 的变量不同 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]();
    }

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

  • 函数 createfunc(i) { return function() { console.log(\'My value: \' + i); }; } 不是仍然闭包,因为它使用了变量 i?

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

  • @Wladimir:您认为 .bind() 是“正确答案”是不正确的。它们各有各的用处。使用 .bind() 时,您无法在不绑定 this 值的情况下绑定参数。此外,您还会获得 i 参数的副本,但无法在调用之间对其进行变异,而这有时是必要的。因此,它们是完全不同的构造,更不用说 .bind() 实现历来很慢。当然,在简单的示例中,两者都可以工作,但闭包是一个需要理解的重要概念,这就是问题所在。

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

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

  • 尝试:

    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 more recent answer about using .bind 当你不需要或不想弄乱 _.partial 's bind ,也可以使用短划线/下划线 thisArg .

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

  • 另一种尚未提及的方法是使用 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]();
    }

    更新

    正如@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]();
    }
  • 这也是我最近做的事情,我也喜欢 lo-dash/underscore 的 _.partial

  • .bind() 将在很大程度上随着 ECMAScript 6 功能的出现而过时。此外,这实际上会在每次迭代中创建两个函数。首先是匿名函数,然后是 .bind() 生成的函数。更好的用法是在循环外创建它,然后在循环内使用 .bind() 来创建它。

  • 引用 14

    @squint @mekdev - 你们两个都对。我最初的例子写得很快,是为了演示如何使用 bind。我根据你们的建议添加了另一个例子。

  • 我认为不必在两个 O(n) 循环中浪费计算,只需执行 for (var i = 0; i < 3; i++) { log.call(this, i); }

  • 引用 16

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

    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 中的任何异步功能中使用。

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

  • 引用 18

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

  • 有点晚了,但是我今天正在探索这个问题,并注意到许多答案并没有完全解决 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]();
    }

    就像之前一样,每个内部函数输出分配给 的最后一个值 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;
    }

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

    Closures

    这也意味着内部函数“保留”外部变量并使其保持活动状态,即使外部函数返回也是如此。为了利用这一点,我们创建并调用一个包装函数,纯粹是为了创建一个新范围, 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]();
    }

    在包装函数内部创建内部函数会为内部函数提供一个只有它自己才能访问的私有环境,即“闭包”。因此,每次调用包装函数时,我们都会创建一个具有自己独立环境的新内部函数,确保变量 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);
        };
    }

    更新

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

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

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

返回
作者最近主题: