闭包
JavaScript闭包的本质:基于词法作用域和将函数按值传递。
解释:
函数在创建时会记住并访问所在的词法作用域,并且保持对词法作用域的引用,就会形成闭包。
由于外部函数无法直接访问内部函数,所以将内部函数return,以间接方式访问此词法作用域中的变量。
var num = 100; //全局作用域
function sum() {
var num = 0; //函数全局作用域
function print() {
num += 2; //函数内部作用域
console.log(num);
return num;
}
return print; //返回函数
}
var fn = sum();
fn(); //2
闭包原理
这篇笔记中含有更加详细的描述:数据存取之闭包与作用域
内层访问外层函数变量
闭包之所以可以访问外层函数中的变量,是因为闭包不只有自己的活动对象还有外层函数的活动对象。
延长作用域链生命周期
一般来说,随着外层函数执行完成,外层函数的执行环境会随之销毁,但由于闭包的影响,在闭包函数中的作用域链与外层函数的执行环境中的作用域链有着相同的引用,因此垃圾回收机制无法回收,所以闭包延长了作用域链的生命周期。
闭包中的易错点
- 内层函数不使用外层函数变量无法形成闭包。
这是一个简单的闭包使用。
function sum() {
var num = 0;
function print() {
num += 2;
return num;
}
return print;
}
var fn = sum();
console.log(fn()); //2
console.log(fn()); //4
当内层函数没有使用外层函数变量时。
function sum() {
let num = 0;
function print() {
let num = 100;
num += 2;
return num;
}
return print;
}
var fn = sum();
console.log(fn()); //102
console.log(fn()); //102
2.循环闭包
举例:
for (var i = 1; i <= 6; i++) {
setTimeout(function print() {
console.log(i);
}, 1000);
}
//预期结果:1 2 3 4 5 6
//实际结果:6 6 6 6 6 6
解释:虽然setTimeout的回调函数是异步的,但for循环是同步的且立即执行的,所以当setTimeout回调函数执行时,此时的i早已循环完变成6了。
解决方案一:立即执行函数。
for (var i = 1; i <= 6; i++) {
(function(num) { //num为形参
setTimeout(function print() {
console.log(num);
}, 1000);
})(i); //给形参num传值
}
//预期结果:1 2 3 4 5 6
//实际结果:1 2 3 4 5 6
解决方案二:给setTimeout套上一个函数。
for (var i = 1; i <= 6; i++) {
print(i);
}
function print(num) {
setTimeout(function print() {
console.log(num);
}, 1000);
}
//预期结果:1 2 3 4 5 6
//实际结果:1 2 3 4 5 6
解决方案三:使用ES6中的let。
for (let i = 1; i <= 6; i++) {
setTimeout(function print() {
console.log(i);
}, 1000);
}
//预期结果:1 2 3 4 5 6
//实际结果:1 2 3 4 5 6
小结:这三种方法归根究底都是创造了一个独立的作用域,使得每个被传递的变量i变成独立的变量不会被for循环改变。
总结
闭包形成的三要素:
- 外层函数包裹内层函数
- 内层函数引用外层函数的变量
- 外层函数返回内层函数