for循环与setTimeout、事件绑定
闭包
闭包 = 函数 + 创建该函数的环境
问题
//循环中为不同的元素绑定事件,事件回调函数里如果调用了跟循环相关的变量,则这个变量取循环的最后一个值 for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } for (var i = 0; i < 5; i++) { setTimeout(function(){ console.log(i) },10) }
这些问题出现的原因,闭包不是主因,是由于setTimeout和事件绑定的机制造成的。
setTimeout是从任务队列结束的时候开始计时的,如果前面有进程没有结束,那么它就等到它结束再开始计时。在这里,任务队列就是它自己所在的循环。循环结束setTimeout才开始计时,所以无论如何,setTimeout里面的i都是最后一次循环的i。
解决方案
setTimeout
setTimeout第一个参数需要一个函数,所以返回一个函数给它,返回的同时把i作为参数传进去,通过形参v缓存了i,并带进返回的函数里面。(算是闭包)
for (var i = 0; i < 5; i++) { var a = function(v){ return function(){ console.log(v) } } setTimeout(a(i),0) }
for (var i = 0; i < 5; i++) { (function(j){ setTimeout(function(){ console.log(j); },0) })(i); }
解决方法还可以用let——使用 let 来声明块变量,这时候变量就能作用于这个块所以能够保存下来。
-
总结
setTimeout的机制:在for循环的时候,settimeout在挂回调,但是,得等到任务队列的最后,也就是for循环结束的时候才执行回调。所以,如果用var的话,var不是块级作用域,var的值在任务最后就是for循环结束的时候,已经变成5了。解决的中心思想就是保存变量,保存变量、保存状态的方法是用闭包。而let的方法(let底层实现也是用闭包。。。),块作用域,和传形参保存变量是一样的,相当于在挂回调的时候包了一层,挂回调的时候比如创建了很多j,在真正调回调的时候,调用的不是i而是保存的j,所以保存下来了。let i在for循环的每次叠代都为i创建新的绑定。
let翻译
for (let i = 0; i < 5; i++) { setTimeout(function(){ console.log(i) },10) } // babel翻译之后 var _loop = function _loop(i) { setTimeout(function () { console.log(i); }, 10); }; for (var i = 0; i < 5; i++) { _loop(i); }
绑定事件
for (var i = 0; i < 5; i++) { var a = function(){ console.log(i) } document.body.addEventListener('click',a) }
事件是需要触发的,而绝大多数情况下,触发的时候循环已经结束了,所以循环相关的变量就是最后一次的取值。
为了解决,通过函数的实参传进函数体。
for (var i = 0; i < 5; i++) { var a = function(v){ return function(){ console.log(v) } } document.body.addEventListener('click',a(i)) }