https://developer.mozilla.org/zh-CN/docs/JavaScript/Guide/Closures
MDN上描述闭包的章节阐述了一个因为闭包产生的常见错误,代码片断是这样的html
for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } }
简言之就是循环中为不一样的元素绑定事件,事件回调函数里若是调用了跟循环相关的变量,则这个变量取循环的最后一个值。segmentfault
因为绑定的回调函数是一个匿名函数,因此文中把形成这个现象的缘由归结为 这个函数是一个闭包,携带的做用域为外层做用域,当事件触发的时候,做用域中的变量已经随着循环走到最后了。数组
注:闭包 = 函数 + 建立该函数的环境浏览器
我对此产生了不少疑问,若是说闭包是函数和建立时的环境,那么事件绑定的时候(也就是这个匿名函数建立的时候),循环中的环境应该是循环当次,为何直接到最后一次了呢?下面咱们就一步一步分析,到底是什么缘由形成的。缓存
为了搞懂这个问题,咱们从最简单的循环开始闭包
for (var i = 0; i < 5; i++) { console.log(i) }
毫无疑问,i会被逐次打印出来ide
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
在循环中会怎样呢?ui
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
测试文档