1. 概念
闭包函数:声明在一个函数中的函数,叫做闭包函数。
闭包:内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)之后。
2. 特点
- 可以实现在外部访问函数内部变量。
- 可以避免全局污染。
- 局部变量常驻内存,会造成内存泄漏。
3. 作用域链
在js中,常见作用域分为全局作用域、函数作用域、块级作用域,作用域有上下级关系,上下级关系的确定就看函数是在哪个作用域下创建的,如果要搞清楚闭包问题,就需要了解作用域链。
如图,当c函数中没有x,y变量时就会按照作用域链往上级找直到找到同名变量,作用域链:c函数作用域->b函数作用域->a函数作用域->全局作用域
4.案例
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i++);
}, 2000)
}
console.log(i);
/* 输出 5 5 6 7 8 9 */
执行步骤
1.全局执行上下文进入执行栈
2.每次执行1个for循环将会发生:将1个setTimeout任务放到浏览器定时触发线程,到达时间后将回调放入任务队列(事件循环),i++
4.执行for循环外的输出任务(输出5)
5.全局执行上下文出栈,同步代码执行完毕
6.任务队列的任务依次进栈->执行(console.log(i++))->出栈(每次栈里只有一个任务)(5 6 7 8 9)
7.执行完毕
由于定时器回调函数未定义i,输出的i根据作用域链找到全局的i(由于存在闭包,全局执行完 i不会被回收),回调执行时i已经是变成5了。
解决方法1:使用自执行函数
for (var i = 0; i < 5; i++) {
(function (x) {
// var x = i;相当于有这么一句
setTimeout(function () {
console.log(x++);
}, 2000)
})(i);
}
console.log(i);/* 结果 5 0 1 2 3 4 */
执行步骤与修改前大致相同,区别在于每次循环都会将自执行函数进栈出栈,也就是说每个自执行函数对应的x在不同内存空间的,定时器访问的x是各个自执行函数中的独有x,而不是全局的i,所以输出的值不会受i后续变化的影响。
解决办法2:使用let声明
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i++);
}, 2000)
}
/* 输出 0 1 2 3 4 */
首先说明let和var的区别,let相对于var具有不支持变量提升、不可重复定义、块级作用域等特点,由于let块级作用域的特点,for循环中每一层循环对应一块作用域,每个回调函数寻找的上级的i也是属于不同作用域的。