JavaScript中的循环和闭包

看一段比较经典的错误代码:

// 希望获取页面上的所有div,在点击的时输出对应的编号
var oDom = document.querySelectorAll("div");

// 事实上,所有的div被点击输出的都是div的个数加1
for (var i = 0; i <= oDom.length-1; i++) {
  oDom[i].addEventListener("click", function log() {
    console.log(i+1);
  }, false);
}

// 希望每秒输出对应的编号,事实上输出的全是6
for (var i = 1; i <= 5; i++){
  setTimeout(function timer() {
    console.log(i)
  }, i* 1000);
}

理解上的错误

这两段代码是JavaScript新手很难理解的地方,为什么就不是我希望的结果?这是曾经让我抓狂不已的东西,但我现在懂了。

下面,我来分析一下上面两段代码:
第一段代码中,使用querySelectorAll获取DOM元素,得到的是一个类数组对象NodeList,可以遍历。
此处使用了for循环遍历,目的是,用addEventListener给每一个div都绑定一个click事件,
事件处理函数是输出每一个div所在的编号,比如如果是页面上的第一个div,就会输出1,但输出的却是页面上div的数目加1。
问题其实就在于循环缺陷就是我们假设每一次迭代在运行时都会给自己捕获一个i的副本
所有的事件绑定函数虽然都是在各自的迭代中定义的,但它们都是被封闭到了共享的全局作用域,因为for循环是没有块级作用域的。
在共享的全局作用域下,实际上只有1个i。循环结束后,i的值就是div.length+1,因此输出的是div的数目加1。

第二段代码中,使用了setTimeout延迟函数,目的是每一秒都输出对应的编号,但输出的全是6。
这里的缺陷是:延迟函数的回调总是会在循环结束才执行。即使setTimeout(..., 0),所有的回调依然是在循环结束后才执行的。
循环结束后的运行结果跟第一段代码的运行原理一样。

解决方法

以上两段话,如果理解了,如何让它变成我们希望的结果呢?
答案的本质是让for变成一个封闭的块级作用域

解决方法有两个:
IIFElet,前者是利用IIFE来创建一个块级作用域,也就是所谓的闭包作用域,后者是利用let的特性会自动转换为块级作用域。
代码如下:

// 第一段 IIFE
for (var i = 0; i <= oDom.length-1; i++) {
  (function(i) {
    oDom[i].addEventListener("click", function log() {
      console.log(i+1);
    }, false);
  })(i)
}
// 第一段 let
for (let i = 0; i <= oDom.length-1; i++) {
  oDom[i].addEventListener("click", function log() {
    console.log(i+1);
  }, false);
}
// 第二段 IIFE
for (var i = 1; i <= 5; i++) {
  (function (i) {
    setTimeout(function timer() {
      console.log(i);
    }, i * 1000);
  })(i);
}
// 第二段 let
for (let i = 1; i <= 5; i++){
  setTimeout(function timer() {
    console.log(i)
  }, i* 1000);
}

如果有新手再问你这个问题,你可以自豪地跟他说:这个问题曾经让我抓狂,但我现在懂了。然后花一点时间,给他好好整整思路。

参考

  • 你不知道的JavaScript

猜你喜欢

转载自www.cnblogs.com/wljqds/p/11295844.html