欢迎大家来到"吊打面试官"系列,我在这个系列中,会和大家分享各种面试中的知识和“坑”,欢迎大家关注我,精彩内容不错过,如果碰到感兴趣的题目想讨论,欢迎大家通过留言告诉我,谢谢,但请记住:
面试或许可以应付,但不要糊弄自己,彻底掌握知识本身才是提升的关键 —— 我说的
闭包是面试和日常开发中都很常见的知识点,但是很多人没有搞清楚它的真实含义,看了一篇又一篇的文章越看越糊涂,本文将帮你彻底理解闭包,欢迎点赞、收藏、评论、转发
内容大纲
- 闭包到底是个啥?
- 闭包有什么用?
- 面试&实例解析
- 总结
- 更新&补充
1-闭包到底是个啥?
正式开始前,先给大家三个结论,暂时看不懂没关系,先记住,读到最后你就会彻底明白:
- JavaScript中,每个函数都会创建自身的闭包
- 闭包没有所谓“用处”或者“好坏”,闭包是JS本身的一部分,你喜欢也好不喜欢也好,它就在那里
(function (){})()
不是闭包,这是个自执行函数
简单来说,闭包给了函数访问外部变量的能力,并将函数与外部变量(函数的环境)绑定在一起
看到这里也许大家依然不明白闭包到底是个啥,那么请先跟我一起搞清楚两个问题,就会明白了:
- 什么是作用域
- 什么是生存周期
1.1-作用域
所谓作用域,就是一个东西(变量、函数、其他)起作用的范围,看个例子
function fn1() {
//1-定义一个变量
let str = 'Blue真帅';
}
function fn2() {
//2-试图使用这个变量
alert(str); //报错:str is not defined
}
fn1();
fn2();
打开F12我们可以看到,上面的例子报错了,当然,这很正常
局部变量,就像函数的私有财产,局部变量只能在定义它的函数内使用
但是,凡事有例外,局部变量在js也是一样,我们再来看另一个例子
function fn1() {
let str = 'Blue真帅';
function fn2() {
alert(str); //正常弹出
}
fn2();
}
fn1();
原因非常简单,因为在JS中,子函数可以直接使用父函数的局部变量,就像人类社会,如果别人拿你的东西可以报警,儿子拿你的东西,警察都懒得管
而这是我们的第一个结论
1.2-生存周期
从垃圾回收说起
说起生存周期,我们需要先从“垃圾回收”这个概念说起,我们都知道,计算机的资源是有限的,所以用完的东西就要尽快释放掉,而垃圾回收粗略的可以分两个阶段:
- 手动回收:C/C++为代表,需要
malloc
/free
或者new
/delete
来手动回收- 如果程序员不够小心,就会产生内存泄漏的问题
- 自动回收(Garbage Collection):Java、JS、Python等高级语言为代表
- 自动回收内存,方便又安全,妈妈再也不用担心我内存泄漏的问题
GC(自动垃圾收集机制)是非常方便的,但垃圾回收需要一定的标准,不能把别人还在用的东西回收了
别人钱包掉了,直接就捡走了,警察来了还说我这不是偷,是回收垃圾,这就肯定说不过去了
所以这时候,有一个问题就很重要了——哪些变量是垃圾,可以被回收?
作用域决定了谁是垃圾
我们直接看一个程序,大家很容易找到感觉
function show() {
//我们有一个局部变量a
let a = '欢迎大家阅读blue的教程,如果喜欢,请点赞,谢谢';
}
//粗略的分为三个阶段:
//1.show执行前:a是不存在的,不占空间
show(); //2.show执行时:a被分配空间
//3.show结束后:a的生存周期结束,可以被回收
所以,局部变量(通常)在函数执行后,就会被回收(当然,变量其实并没有被立即回收,而是被标记为“可回收”,在下一次GC工作时被带走)
那……跟闭包有啥关系
相信上面说的,大家都明白,但是跟我们说的闭包有啥关系呢?别急,再看个例子
function show() {
let str = '欢迎观看blue的教程';
document.onclick = function() {
alert(str);
};
}
//执行前,str不存在
show(); //执行时,str被创建
//执行后,str“本应”被回收
在上面的例子中,我们show中的局部变量str,本应在函数结束后被回收(局部变量在函数结束后被回收),但是实际情况是,不论你10分钟还是1天以后点击页面,这个str都还在,并没有被回收,原因很简单
函数的存在,延长了外层局部变量的生存周期,只要这个onclick函数还在,那么它(onclick函数)外面的局部变量就不会回收
但是这时候还有三个延伸的问题:
- js怎么知道你用了哪些局部变量的?要保留哪些局部变量不回收?
- js如何确定onclick消失后要回收哪些变量?
- 父级的变量不回收,那父级的父级呢?
延伸问题1-js如何确定哪些父级变量要保留?
先说结论:js不需要确定,它直接保留全部父级变量,用没用到都保留
来看个例子
function show() {
let a = 12;
let b = 5;
document.onclick = function() {
alert(eval(prompt('请随便输入一个表达式')));
};
}
show();
在这个例子中,我们并没有显式的引用任何一个父级变量,而是完全用运行时eval来运行,但我们可以看到,a和b其实都在
这时大家可能会想,“用不用到都保留,那这多浪费资源啊”、“js为什么不识别一下啊”,啥的,其实原因很简单
- 健壮性:代码是一个极其复杂的东西,理论上官方不可能100%不出错的确定哪些用了哪些没用,而如果误回收了变量,会导致程序崩溃,所以全都保留是出于程序健壮性的考虑
- 性能考虑:就算假设有办法判定一段代码里到底用了什么,也会是非常复杂的,会造成大量的运行时开销,反而造成程序性能降低,得不偿失
结论是,闭包会保留全部父级变量,不论用没用,这是非常正确的选择
延伸问题2-引用计数机制
那js如何知道在onclick失效时去回收哪些变量呢?这就要说到引用计数机制,简单看个例子:
let a = { name: "blue", age: 18 }; //{json}被a引用,所以json的引用计数加1
//引用计数=1
let b = a; //{json}又被b引用,引用计数为2
//引用计数=2
a = null; //a不再引用{json},引用计数减1,还剩1个
//引用计数=1
//此时依然有b在引用{json},所以并不回收
b = null; //引用计数又减1
//引用计数=0
//至此,{json}被标记为“可回收”
所谓引用计数,就是标明一个东西,在被几个人引用,一旦引用计数归零,则被标记为“可回收”
而上面的onclick也是一样的道理
function show() {
let a = 12; //引用计数:1
document.onclick = function() {}; //引用计数:2
}
show(); //show运行中,引用计数:2
//函数结束,let a相关的引用消失,但并不为零,因为onclick还在引用自己的父级变量
//引用计数:1
//直到有一天...
document.onclick = null; //function没了,所以引用计数减1
//引用计数:0
//a标记为“可回收”
所以,js(包括java等依赖gc的语言都是)依靠引用计数机制,确定一个东西是否能回收
延伸问题3-父级的父级呢?
闭包的存在,不仅延长父级的局部变量有效期,父级的父级、父级的父级的父级...一直到全局的,其实都会延长
function fn1() {
let a = 12; //保留
function fn2() {
let b = 5; //保留
document.onclick = function() {
//a和b在onclick中都有可能被用到,所以,都延长
};
}
fn2();
}
fn1();
1.3-什么是闭包
把我们上面说的所有东西结合起来,闭包的结论就出来了
为了帮大家加深印象,这里放一个官方对js闭包的定义
developer.mozilla.org/en-US/docs/…
2-闭包有什么用?
那么,大家已经掌握了闭包的含义,可是这东西有什么用呢?其实,“没什么用”:
- 闭包是js自身语法的一部分,只要你访问了父函数的局部变量,都算是“用到了”闭包,这是一个自然的过程,无需刻意使用
- 闭包保证了js自身的运行,如果错误的将外部变量回收掉,会导致js崩溃,所以闭包更多的是为js语言本身服务,而不是我们
不过,既然了解了闭包的机制,我们确实可以在写代码时注意一些问题,让我们的代码更加高效
性能方面的建议
当你使用任何函数时,记得它会创建一个闭包,也就是说,在这个函数失效前,所有外层变量都会被保留,如果有一些大型对象(例如组件、复杂的数据、图片之类的),会消耗大量资源
来看个例子吧
/*
假设,我们有一个用于初始化canvas的函数,它需要做几件事
1-读取图片
2-绘制到画布
3-定义一些操作
*/
function initCanvas() {
let img = new Image();
img.src = 'http://xxx/一张很大的图片.jpg';
let gd = canvas.getContext('2d');
gd.drawImage(img); //为了简化代码,没有加onload
//问题出现!
canvas.onclick = function() {
// 一些操作
};
}
上面的代码有个问题,onclick会延长img变量的生存周期,而img属于很占资源的东西,所以会导致大量内存浪费
不过不用担心,有很多办法优化这个东西,比如块级作用域、拆分函数等,随便举个例子
function initCanvas() {
let img = new Image();
img.src = 'http://xxx/一张很大的图片.jpg';
let gd = canvas.getContext('2d');
gd.drawImage(img);
}
//img对象,会在initCanvas结束后顺利回收
function initEvent() {
canvas.onclick = function() {
// 一些操作
};
}
//onclick并没有跟img发生关系,所以,它不会使img一直被保留
3-面试&实例解析
实例1:真·绝对私有成员
面试题:JavaScript中实现私有属性
简单说一下前因,js目前还不支持真正的私有成员
class Box {
private name = 'blue'; //报错:js没有private关键字(目前)
}
所以,解决方法也非常简单,直接上局部变量就行
(function() {
let name = 'blue'; //这个变量,是任何人从外部都访问不到的
})();
然后
(function() {
let name = 'blue';
//Box中的方法可以访问name,因为“闭包”
class Box {
get name() {
return name;
}
set name(val) {
//如果需要,可以校验一下val
name = val;
}
}
})();
最后,就可以返回出去用了
//2-接收这个class,以便在外部使用
const Box = (function() {
let name = 'blue';
//1-不直接暴露name,而是class出去
return class {
get name() {
return name;
}
set name(val) {
name = val;
}
}
})();
let box = new Box();
//3-妄图直接使用name
name = 'xxx'; //报错
//4-只能通过setter来访问
box.name = 'name2'; //正确
实例2:烂大街的i
这是一个非常经典的案例,大家也许见过,不过从闭包的层面再来说说,能加深理解
首先,一个很简单的代码
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>欢迎大家来到blue的教程</title>
</head>
<body>
<button type="button">点赞</button>
<button type="button">收藏</button>
<button type="button">加关注</button>
<script>
let aBtn = document.querySelectorAll('button');
//注意这里的i,是var定义的,这个问题在let下已经不存在了——后面会说
for (var i = 0; i < aBtn.length; i++) {
aBtn[i].onclick = function() {
alert(i); //这里很简单,就是想点“第几个按钮”就“弹几”
};
}
</script>
</body>
</html>
看起来很简单对吧?但是运行效果非常“惊人”
你会发现,不论点哪个按钮,出来都是“3”,这太邪乎了吧?不会的,万事万物都有原因
问题1:为什么是3
任何语言中(当然包括js),变量虽然叫“变”量,但它不会没事儿自己变来变去的
变量会保留最后一次赋给它的值,直到下次赋值为止
所以我们拆解一下这个程序,就很容易理解
for (var i = 0; i < aBtn.length; i++) {
aBtn[i].onclick = function() {
alert(i);
};
}
//完全等价于
var i = 0;
{
aBtn[i].onclick = function() {
alert(i);
};
i++;
}
{
aBtn[i].onclick = function() {
alert(i);
};
i++;
}
{
aBtn[i].onclick = function() {
alert(i);
};
i++;
}
但是,为什么是3?
再变一下
var i = 0;
{
i++;
}
{
i++;
}
{
i++;
}
alert(i); //你猜,i等于几?
结果当然是3
因为onclick执行时,i早就加到3了,所以不是3才奇怪
问题3:为什么let没事
但是,更好玩的事发生了,这里如果我们用let,这个问题就消失了
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<button type="button">点赞</button>
<button type="button">收藏</button>
<button type="button">加关注</button>
<script>
let aBtn = document.querySelectorAll('button');
//啥也没改,就是var变成let了
for (let i = 0; i < aBtn.length; i++) {
aBtn[i].onclick = function() {
alert(i);
};
}
</script>
</body>
</html>
你会(又)震惊的发现,它居然好了
原因很简单,因为let带有块级作用域
{
var a = 12;
}
alert(a); //能出来,因为var是函数级作用域
{
let b = 5;
}
alert(b); //报错,因为let带有块级作用域,仅在定义它的代码块中生效
那为什么let能解决上面的问题?
//let带有块级作用域
//“大家一人一个i,谁也别跟别人抢哈”
{
let i=0;
aBtn[i].onclick = function() {
alert(i);
};
}
{
let i=1;
aBtn[i].onclick = function() {
alert(i);
};
}
{
let i=2;
aBtn[i].onclick = function() {
alert(i);
};
}
问题4:如何用闭包的思想解决这个问题
这个问题现在随着let的流行,其实已经不是问题了,但是没关系,我们就是要用一用闭包,咋滴吧
思路很重要:
- let没问题,因为let是块级作用域,所以“一人一个i”
- var有问题,因为var是函数级作用域,所以“大家抢一个i”
那么,解决的思路就来了,var能不能也做到一人一个呢?当然,加个函数就好
let aBtn = document.querySelectorAll('button');
for (var i = 0; i < aBtn.length; i++) {
//参数,其实相当于一个局部变量,从而达到一人一个index的目的
(function(index) {
aBtn[index].onclick = function() {
alert(index);
};
})(i);
}
当然,有些时候为了方便(同时提升代码B格),我们可以让参数也叫i,这没什么,参数和外部变量同名没关系的
let aBtn = document.querySelectorAll('button');
for (var i = 0; i < aBtn.length; i++) {
//不用找哈,只有参数名字变了
(function(i) {
aBtn[i].onclick = function() {
alert(i);
};
})(i);
}
4-结论
是时候梳理一遍我们讲过的东西了,那么首先
我们来总结一下,这次说到的内容:
- 闭包=函数+外层变量
- 闭包的第一重含义:子函数可以访问父函数的局部变量
- 闭包的第二重含义:子函数的存在,延长了外层变量的生存周期(垃圾回收、引用计数)
闭包的应用:
- 闭包是JS自身语法的一部分,99%的情况下不需要刻意使用它,它一直在发挥作用
- 优化建议:注意所有子函数,都会延长父级变量的存在时间(图片等大型数据对象的性能问题)
- 典型应用1:真·绝对私有成员
- 典型应用2:解决i的问题(现在其实let更好了)
5-有bug?想补充?
感谢大家观看这篇教程,有任何问题或想和我交流,请直接留言,发现文章有任何不妥之处,也请指出,提前感谢