「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。
一、初识闭包
还记得初识闭包时,在书上看到闭包概念是这样的:
《JavaScript高级程序设计(第3版)》:闭包,是指有权访问另一个函数作用域中的变量的函数。
《JavaScript高级程序设计(第4版)》:闭包,指的是那些引用了另一个函数作用域中变量的函数,通常在嵌套函数中实现的。
从上述书本定义来看,我们需要先了解作用域,才能真正理解啥是闭包。那么接下来我们先学习下作用域,再来揭开”闭包”的神秘面纱吧~~
二、相关概念
作用域&作用域链
1、作用域
概念
作用域,简单来讲就是变量的可用范围。
为什么需要作用域?
作用域,是用来防止不同范围的变量之间互相干扰。
作用域类型
1)全局作用域
不属于任何函数的外部范围称为全局作用域。
全局变量,保存在全局作用域的变量。
优点: 可反复使用
缺点: 全局污染
2)函数作用域
一个函数内的范围称为函数作用域。
局部变量,保存在函数作用域内的变量。
优点: 不会被污染
缺点: 无法反复使用
2、作用域链
js规定,一个函数,既能用自己作用域的变量,又能使用外层作用域的变量。当查找一个变量时,当前函数先寻找,未找到继续在上层作用域查找,由内向外,直到全局作用域,因而形成链条,就叫作用域链。
作用域链,就像生活中我们规划景点“游览线路”一样,由近及远。
作用域的本质
JS中,作用域和作用域链都是对象结构。我们先来看个demo案例,debug断点来探究下作用域的本质:
由上图可知:
- 函数作用域其实是一个js引擎临时创建的对象,只不过没有对象名。
- 函数作用域对象中保存着函数内所有局部变量。
继续调试代码,当执行调用完fun()函数,观察作用域的变化,如下图:
总结:
- 函数作用域,其实是js引擎在调用函数时才临时创建的一个作用域对象(也叫活动的对象AO),其中保存函数的局部变量。而函数调用完,函数作用域就释放了。
- 全局作用域是Global作用域对象(GO)。
函数调用过程 VS 做饭三步曲
第1步:准备食材
第2步:做菜
第3步:收拾厨房
三、再探闭包
从用法上理解闭包
给一个函数,保存一个既可反复使用,又不会被外界污染的专属局部变量,该函数就是闭包。
使用闭包“三步曲”
第1步:
用外层函数包裹要保护的变量和使用变量的内层函数。
第2步:
在外层函数内部返回内层函数对象。
第3步:
调用外层函数,用变量接住返回的内层函数对象。
// 第1步:用外层函数包裹要保护的变量和使用变量的内层函数
function mother() {
var total = 1000;
// 第2步:返回内层函数对象
return function(money) {
total -= money;
console.log(`花了${money}还剩${total}元`)
}
}
// 第3步:调用外层函数,用变量接住内层函数对象
var pay = mother();
// pay接住的是mother返回的内层函数
pay(100);// 剩900
total = 0;
pay(100);// 剩800
闭包的产生原因
闭包的出现,是为了实现既重用变量又保护变量不被污染的需求。
闭包的本质
闭包也是一个对象,即每次调用外层函数,临时创建的函数作用域对象。
一句话概括:闭包如何形成的?
外层函数调用时,外层函数的作用域对象被内层函数引用着,无法释放,就形成了闭包对象。
闭包结构 VS 妈妈发小宝宝红包
闭包结构,我们可以结合生活中妈妈生小宝宝的场景类比理解:
- 外层函数——怀了小宝宝的妈妈
- 内层函数(多个)——小宝宝(们)
- 外层函数的局部变量——妈妈给宝宝的专属红包
- 调用外层函数妈妈,生了小宝宝,小宝宝拿着属于自己的红包,自立门户!
闭包的优缺点
优点
使用闭包的优点是可以避免全局变量污染。
缺点
闭包中,由于内层函数一直引用着外层函数的作用域对象,不能及时释放,极容易造成内存泄露。
解决闭包造成的内存泄露
及时释放不用的闭包:
将保存内层函数对象的变量赋值为null,内层函数引用的闭包对象没人要了,闭包对象就被释放回收了。
function mother() {
var total = 1000;
// 第2步:返回内层函数对象
return function pay(money) {
total -= money;
console.log(`花了${money}还剩${total}元`)
}
}
var pay = mother();
// 不用时,及时释放闭包
pay = null;
四、闭包的笔试/面试题
考察闭包知识点,主要会出如下两类题型:
类型一:看程序,判断输出
1)顺产1个、剖腹产1个孩子(公用红包)
function fun(){//妈妈
var i=999;//红包
//给从未声明过的变量赋值,自动在全局创建该变量
//剖腹产
nAdd=function(){i++};//孩子1
//妈妈顺产生了一个孩子2
return function(){//孩子2
console.log(i)
}
}
var getN=fun(); //妈妈包了一个红包,生了2个孩子 //结果: 两个孩子共用同一个红包!
getN();//红包=999
nAdd();//(红包+=1)红包=1000
getN();//红包=999
// 输出结果:999、1000
2)生了2次孩子(独享红包,互不影响)
function mother() {//妈妈
var i = 0; //红包
return function () {//孩子
i++;
console.log(i);
};
}
var get1 = mother(); //妈妈生老大,给老大包一个红包
get1();//老大的红包是1
var get2 = mother(); //妈妈又生了老二,又给老二包一个新的红包
get2();//老二的红包是1
get1(); //老大的红包+1,变成2
get2(); //老二的红包+1,变成2
// 输出结果:1、1、2、2
3)剖腹产3个孩子(公用红包)
function fun(){//妈妈
arr=[];//不算红包,因为内层函数中没有用到!
//但是arr是给未声明的变量强行赋值
//自动在全局创建该变量
for(var i=0/*红包*/;i<3;i++){//循环三次 //强行给外部全局变量中添加新函数——破腹产
// new Function()
arr[i]=function(){//反复创建了3个孩子
console.log(i);//因为只是创建函数,所以这里暂时不执行。
}
}
//for结束后,i=3
}
fun(); //妈妈一次刨妇产生了3个孩子,包了一个共用的红包,红包里是3块钱
arr[0]();//3
arr[1]();//3
arr[2]();//3
// 输出结果:3、3、3
类型二:根据需求,用闭包实现功能
修改下面的代码,让循环输出的结果依次为0, 1, 2, 3, 4:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
}
// 输出结果:5、5、5、5、5
1)使用闭包
for (var i = 0; i < 5; i++) {
log(i);
}
function log(i) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
}
2)立即执行函数
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
3)es6块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
}