前置知识: 作用域链
概念汇总
经典应用
循环会丢失当前值
for(var i = 1; i<=5;i ++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
上文代码,理想情况是输出1 2 3 4 5,但实际上大家都知道,会输出5个6。
这是因为 i 的当前作用域与 for 循环一样,他们共享一个全局作用域,也即是 i 指向一个全局的引用。
这里我们可能会想当然的认为在每次循环的时候,全局的 i 的值也并不是6呀。这里需要提一下 setTimeout:js使用回调函数的方式实现多任务,setTimeout 的回调函数只会在循环结束后被统一调用,而这时拿到的肯定是6。
闭包解法
好,这里再复习一下思维导图里闭包的特性:每个函数的作用域是自己定义时的作用域,那么我们只要在每次循环的时候定义一个闭包,将 i 当时的值保存进去不就好了。
根据这个思路,我们写出下面↓的这个升级版:
for(var i = 1; i<=5;i ++) {
(function() {
var j = i
setTimeout(function() {
console.log(j)
}, 1000)
})()
}
此时一共有三个作用域,for 和 i 所在的全局作用域,包含 j 和 setTimeout的 立即执行函数(IIFE) 所声明出的作用域,包含 console.log 的匿名函数A的作用域。此时 IIFE函数 和 匿名函数A 都是闭包。
在匿名函数A中,寻找变量j的声明,沿着作用域链向上找,发现在IIFE中声明值为 i ,IIFE函数同样是闭包,在其声明时,i 即为当时的 i 值,也就是 1、2、3、4、5,将这个值传给匿名函数A。
由此,可以输出想要的 1、2、3、4、5。
但是这个写法可以再优化一下,同样的内容,将 i 作为一个参数传入 IIFE,减少临时变量的数量,就变成了我们最常见的写法:
for(var i = 1; i<=5;i ++) {
(function(i) {
setTimeout(function() {
console.log(i)
}, 1000)
})(i)
}
块作用域解法
在 ES6 之前的JS中,只有函数、try/catch、with 可以形成块作用域,而我们常用的if、for等并不会,也就是说,在里面用var声明的变量会是其的父作用域
if (true) {
var a = '111'
}
console.log(a) // 111,并没有报错
而在ES6中引入了新的关键字 let ,let关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。换句话说,let为其声明的变量隐式地劫持了所在的块作用域。
if (true) {
let a = '111'
}
console.log(a) // Uncaught ReferenceError
有了块作用域,其实就可以直接在循环中中大胆声明了,因为每个块作用域中的i指向当前循环被声明的i
for(let i = 1; i<=5;i ++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
// 1 2 3 4 5