闭包
闭包 = 函数 + 创建该函数的环境
问题
//循环中为不同的元素绑定事件,事件回调函数里如果调用了跟循环相关的变量,则这个变量取循环的最后一个值
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))
}