资讯专栏INFORMATION COLUMN

[Javascript实验课]循环中的闭包

teren / 2696人阅读

摘要:执行出来的结果是这样的实验发现,无论如何都在最后执行,这证实了我们之前遇到的问题,因为在循环结束才执行,所以回调函数调用的取值必然是循环的最后一次。

前言

https://developer.mozilla.org/zh-CN/docs/JavaScript/Guide/Closures
MDN上描述闭包的章节阐述了一个由于闭包产生的常见错误,代码片段是这样的

for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }

简言之就是循环中为不同的元素绑定事件,事件回调函数里如果调用了跟循环相关的变量,则这个变量取循环的最后一个值。

由于绑定的回调函数是一个匿名函数,所以文中把造成这个现象的原因归结为 这个函数是一个闭包,携带的作用域为外层作用域,当事件触发的时候,作用域中的变量已经随着循环走到最后了。

注:闭包 = 函数 + 创建该函数的环境

我对此产生了很多疑问,如果说闭包是函数和创建时的环境,那么事件绑定的时候(也就是这个匿名函数创建的时候),循环中的环境应该是循环当次,为什么直接到最后一次了呢?下面我们就一步一步分析,究竟是什么原因造成的。

简单循环中的i

为了搞懂这个问题,我们从最简单的循环开始

for (var i = 0; i < 5; i++) {
     console.log(i)
}

毫无疑问,i会被逐次打印出来

for (var i = 0; i < 5; i++) {
    var a = function(){
        console.log(i)
    }
    a()
}

这里,i也会被逐次打印出来,因为js里,外层函数作用域会影响内层,而内层不会影响外层。基于这个原理,我们也可以加多少层都没关系:

for (var i = 0; i < 5; i++) {
    var a = function(){
        return function(){
            console.log(i)
        }
    }
    a()()
}

每一层匿名函数和变量i都组成了一个闭包,但是这样在循环中并没有问题,因为函数在循环体中立即被执行了。setTimeout和事件则不太一样,详见下文。

setTimeout在循环里

-setTimeout在循环中会怎样呢?

for (var i = 0; i < 5; i++) {
    setTimeout(function(){
        console.log(i)
    },10)
}

不出所料,这里果然出问题了,打印出来的结果为5个5,遇到了前言中所述的由于闭包所引起的常见错误。

根据内部可调用外部作用域的原理,setTimeout的回调函数里面调用了外层的i,i和回调函数组成了闭包。i在循环执行之前是0,循环之后是5。

一切都顺理成章,很好理解,问题就是为什么setTimeout的回调不是每次取循环时的值,而取最后一次的值,难道setTimeout回调是在循环体外触发的?

会不会是时间的问题,我们把setTimeout的回调延迟设为0毫秒试一下。

for (var i = 0; i < 5; i++) { 
    var a = function(){
        console.log(i)
    }
    setTimeout(a,0)
}

这并没有解决问题

另注:其实setTimeout的延迟时间是存在最小值的,根据浏览器的不同有可能是4ms 或者5ms,这意味着就算setTimeout设为0,还是有一小段的延迟的。
详见:https://developer.mozilla.org/en-US/docs/Web/API/Window.setTimeout#Notes

为了测试究竟是不是时间的问题,我采用了下面这种更加残暴的方式:

for (var i = 0; i < 100; i++) { 
    var a = function(){
        console.log(i)
    }
    a();
    setTimeout(a,0)
}

循环100次,一次普通调用,一次在setTimeout里面调用,如果存在延迟,那么setTimeout出来的结果会在一个中间点,很难是100。

执行出来的结果是这样的:

实验发现,无论如何setTimeout都在最后执行,这证实了我们之前遇到的问题,因为setTimeout在循环结束才执行,所以回调函数调用的i取值必然是循环的最后一次。

-setTimeout为什么会在最后执行呢,这是因为setTimeout的一种机制,setTimeout是从任务队列结束的时候开始计时的,如果前面有进程没有结束,那么它就等到它结束再开始计时。在这里,任务队列就是它自己所在的循环。循环结束setTimeout才开始计时,所以无论如何,setTimeout里面的i都是最后一次循环的i。

解决办法如下:

for (var i = 0; i < 5; i++) {
    var a = function(v){
        return function(){
            console.log(v)
        }
    }
    setTimeout(a(i),0)
}

很多人能利用上面的方法解决这个问题,因为setTimeout第一个参数需要一个函数,所以返回一个函数给它,返回的同时把i作为参数传进去,通过形参v缓存了i,并带进返回的函数里面。

下面这个方法则不行:

for (var i = 0; i < 5; i++) {
    var a = function(v){
        return function(){
            console.log(v)
        }
    }
    setTimeout(function(){
         a(i)
    },0)
}

这里的问题是,回调函数没有立即执行,本身又没有传入参数缓存

总结:例子中遇到setTimeout的问题,罪魁祸首是回调等待循环队列结束造成的,解决的办法是给回调函数传一个实参缓存循环的数据。

循环中的事件

循环中的事件和setTimeout类似,也会涉及闭包问题,事件的listener,会和循环相关的变量形成一个闭包,在执行listener的时候,变量取最后一次循环的值。

for (var i = 0; i < 5; i++) { 
    var a = function(){
        console.log(i) 
    }
    document.body.addEventListener("click",a) 
}

但是和setTimeout不一样的是,事件是需要触发的,而绝大多数情况下,触发的时候循环已经结束了,所以循环相关的变量就是最后一次的取值,比如上例中,点击body以后console 5次5,通过addEventListener添加的事件是可以叠加的。

考虑下面的代码:

for (var i = 0; i < 2; i++) { 
    var a = function(){
        console.log(i) 
    }
    document.body.addEventListener("click",a) 
}

for (var i = 0; i < 5; i++) { 
    var a = function(){
        console.log(i) 
    }
    document.body.addEventListener("click",a) 
}

答案是:

2次5和5次5,因为两次循环使用了同样的全局变量i,你点击的时候这个i已经变成了5,不管事件是在两次循环里绑定的还是五次循环里绑定的,点击回调只认全局变量i,跟在哪绑定的没关系。

如果我们想要2次2和5次5,就需要把前一次循环放到函数作用域里或者把其中一个i换成别的变量名

(function(){
    for (var i = 0; i < 2; i++) { 
        var a = function(){
            console.log(i) 
        }
        document.body.addEventListener("click",a) 
    }

})()
for (var i = 0; i < 5; i++) { 
    var a = function(){
        console.log(i) 
    }
    document.body.addEventListener("click",a) 
}

至于解法,和setTimeout类似,也是通过listner形参缓存循环中的变量,以下代码中,函数a返回一个函数,因为addeventlistner第二个参数接受的是函数,所以要这么写,而要执行的内容,写在返回的这个函数体内。

for (var i = 0; i < 5; i++) { 
    var a = function(v){
        return function(){
            console.log(v)
        }
    }
    document.body.addEventListener("click",a(i))
}
总结

闭包并没有那么复杂,可以简单的理解为函数体和外部作用域的一种关联。

-setTimeout和绑定事件在循环经常会带来意想不到的效果,取决于这两个函数的特殊机制,闭包不是主因。

如果想在setTimeout和绑定事件保存住循环过程中产生的变量,需要通过函数的实参传进函数体。

参考(感谢以下作者):

http://www.cnblogs.com/hongdada/p/3359668.html

http://www.cnblogs.com/hh54188/p/3153358.html

https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener

https://developer.mozilla.org/en-US/docs/Web/API/Window.setTimeout

http://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)

https://developer.mozilla.org/zh-CN/docs/JavaScript/Guide/Closures

测试文档

http://jsfiddle.net/fishenal/wfU56/3/

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92316.html

相关文章

  • chrome下的Javascript的任务机制

    摘要:在第一次循环的时候并没有被赋值,所以是,在第二次循环的时候,定时器其实清理的是上一个循环的定时器。所以导致每次循环都是清理上一次的定时器,而最后一次循环的定时器没被清理,导致一直输出。 Javascript Evet Loop 模型 setTimeout()最短的事件间隔是4mssetInterval()最短的事件间隔是10ms以上这个理论反正我是没有验证过 Exemple 1 --...

    nidaye 评论0 收藏0
  • 新鲜出炉的8月前端面试题

    摘要:前言最近参加了几场面试,积累了一些高频面试题,我把面试题分为两类,一种是基础试题主要考察前端技基础是否扎实,是否能够将前端知识体系串联。 前言 最近参加了几场面试,积累了一些高频面试题,我把面试题分为两类,一种是基础试题: 主要考察前端技基础是否扎实,是否能够将前端知识体系串联。一种是开放式问题: 考察业务积累,是否有自己的思考,思考问题的方式,这类问题没有标准答案。 基础题 题目的答...

    qingshanli1988 评论0 收藏0
  • JavaScript中的闭包

    摘要:权威指南第版中闭包的定义函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中成为闭包。循环中的闭包使用闭包时一种常见的错误情况是循环中的闭包,很多初学者都遇到了这个问题。 闭包简介 闭包是JavaScript的重要特性,那么什么是闭包? 《JavaScript高级程序设计(第3版)》中闭包的定义: 闭包就是指有权访问另一个函数中的变...

    Donne 评论0 收藏0
  • JavaScript闭包 的详解

    摘要:局部变量,当定义该变量的函数调用结束时,该变量就会被垃圾回收机制回收而销毁。如果在函数中不使用匿名函数创建闭包,而是通过引用一个外部函数,也不会出现循环引用的问题。 闭包是什么 在 JavaScript 中,闭包是一个让人很难弄懂的概念。ECMAScript 中给闭包的定义是:闭包,指的是词法表示包括不被计算的变量的函数,也就是说,函数可以使用函数之外定义的变量。 是不是看完这个定义感...

    longshengwang 评论0 收藏0
  • JavaScript系列——JavaScript同步、异步、回调执行顺序之经典闭包setTimeou

    摘要:同步异步回调傻傻分不清楚。分割线上面主要讲了同步和回调执行顺序的问题,接着我就举一个包含同步异步回调的例子。同步优先回调内部有个,第二个是一个回调回调垫底。异步也,轮到回调的孩子们回调,出来执行了。 同步、异步、回调?傻傻分不清楚。 大家注意了,教大家一道口诀: 同步优先、异步靠边、回调垫底(读起来不顺) 用公式表达就是: 同步 => 异步 => 回调 这口诀有什么用呢?用来对付面试的...

    lewif 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<