一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
前言
面试时总被问到闭包?内心没有概念答不出来?面试前看了一些概念性的总结文章或者背点经典的闭包笔试题,但对底层的原理还是不清楚,面试官针对闭包问题再次深挖或者扩展,就懵逼?来来来,看这里,保证一文彻底搞懂闭包,再他闭包笔试题千变万化,也能轻松应对。
其实平常开发中已经用了很多了,但是自己却不知道那其实就是闭包。
闭包
前奏
开始之前,我们先看一下下面这个栗子:
如果让你写一段代码,小孩过年拿到1000块钱红包,每次花出去100,这个程序怎么写?
同学们肯定说那简直不要太easy,于是乎,出现了下面一顿猛操作
var money=1000;
function fun (){
money-=100;
console.log(money);
}
fun();
fun();
执行了两次,也就是每次花了100块钱,看控制台最后剩了800,这段代码没有错。但是有个问题?如果别的开发小伙伴也用了这个money变量了呢,看看发生了什么?
var money=1000;
function fun (){
money-=100;
console.log(money);
}
fun();
money=0;
fun();
小孩哭了,到手的钱还没怎么花呢,就花了100,怎么还欠银行100呢,对呀,这值怎么就成负数了呢,怎么就-100了呢,明明是800才对呀?
这是因为money是全局变量,随时随地谁都可以用,容易被篡改,容易被污染,如果开发团队中,一个人定义了全局变量,开发中定义的变量太多了,本人都不知道自己定义了什么变量,那就容易被其他成员使用篡改,因此,开发团队中,一般不建议使用全局变量。
有的小伙伴,一想,全局容易被污染,那我定义成局部的不就完事了吗,真的行的通吗?
往下看->
function fun (){
var money=1000;
money-=100;
console.log(money);
}
fun();
money=0;
fun();
运行结果是这样的:
这回换银行该哭了,钱怎么取怎么花剩的钱都是900。这是因为每次调用函数fun都会临时创建一个作用域函数对象,保存局部变量money,也就是重新创建一个局部变量money,执行完函数fun,这个作用域函数对象和局部变量money就会被销毁,下次调用fun,就会重新创建一个临时的作用域函数对象,保存局部变量,执行完,就会销毁。所以,局部变量不可重复使用
以上的痛点就是:第一个变量被污染了,第二个是想重复使用一个变量。
那怎么办呢,全局不行局部也不行,这时候就用到闭包了,
闭包解决的问题就是:既想重复使用一个变量,又不想这个变量被污染
看看闭包是如何解决这个问题:
function fun (){
var money=1000;
return function(){
money-=100;
console.log(money);
}
}
var pay=fun();
pay();
money=0;
pay();
pay();
pay();
pay();
看看效果吧:
是不是完美解决了,这就是闭包了,平常开发中挺常用的吧,用的可能比这复杂的多,这里就是举个栗子,那么闭包到底是什么呢?往下看哦
什么是闭包?
既可以重复使用一个变量,又不会被污染的一种编程方式
什么时候使用?
希望给一个函数,保存一个既可以重复使用,又不会被外界污染的的专属的局部变量时,就使用闭包。
怎么使用?
- 外层函数包裹保护的变量和使用此变量的一个内层函数;
- 外层函数内部返回内层函数;
- 外部调用外层函数并接住返回的内层函数;
//第一步:外层函数包裹被保护的变量和使用此变量的内层函数
function fun (){
//被外层函数包裹保护的变量
var money=1000;
//第二步:外层函数返回使用被保护变量的内层函数
return function(){
//被保护的变量进行操作
money-=100;
console.log(money);
}
}
//第三步:调用外层函数,并接住内层函数对象
//backFun接住的是fun返回的内层函数
var pay=fun();
//调用内层函数
pay(); //900
//其他人执行程,这里money=0没有影响后面程序值,被保护的局部变量并没有被修改为0
money=0;
pay();//800
pay();//700
pay();//600
pay();//500
当外部money=0,没有影响后面程序值,局部变量money并没有被修改为0,
就证明money被保护起来了,只能pay函数调用,调用pay函数,money值连续递减,证明money被重复使用了。
为什么要这样使用?
定义外层函数时,小写fun会被自动翻译为:new Function();
这时候fun也就会有一个自己的地址,这个地址存着fun函数对象,同时形成了一个作用域链对象,
里面目前有两个作用域,一个是window,另外一个是fun自己的作用域对象,
只不过还没有调用执行fun,所以目前还是空的。
开始执行var pay=fun()这行代码,全局作用域里面多了一个pay全局变量,
此时pay值为undefined,因为此行代码还没有执行完。
调用函数fun,此时执行调用函数三部曲第一步:备料,(详情可查看上一篇文章,作用域&作用域链 https://juejin.cn/post/7067564413342973966 )
创建一个临时的函数作用域对象,保存局部变量。
这时,函数fun就有了自己的作用域,并且有一个自己的地址1*5656。
继续执行fun函数,这里开始三部曲的第二步:按照菜谱做菜(根据js引擎执行程序),
这时局部变量money已经被赋值1000,执行到定义一个fun时,自动创建一个新的函数对象,并有自己的地址1*8585,new Function();
最后此内层函数被返回。
此时的内层函数的作用域链上有3级作用域(?,因为:在定义一个函数时,就已经形成了自己的作用域,详情可查看上一篇文章,作用域&作用域链 https://juejin.cn/post/7067564413342973966 ),自己(空),fun(妈妈),window。
因为内层函数并没有被调用,所以,并没有创建自己的临时作用域,因此自己的作用域为空。
还有就是,闭包结构的内层函数比一般函数多了一级作用域,多的那一级是外层功函数也就是自己的妈妈。
耳听为虚,眼见为证:
console.log.dir可以打印出对象的作用域链,可以看出除了自己,里面有两个数据,分别是fun和Global。
这里开始三部曲的第三步:清理厨房,
这里可以看到,pay作用域链里有一级是指向外层函数fun的作用域对象的,
此时函数fun已经执行完毕,紧接着fun函数作用域对象伴随着它的局部变量一块进行销毁,只会销毁离fun最近的一个作用域,也就是它自己。
但是这里,作用域对象并没有被销毁,这是为什么呢?
这是因为:
pay变量也就是内层函数的作用域链里面引用着外层函数fun的作用域对象,因此,外层函数fun的作用域对象无法释放,从而形成了闭包,
而且只有内层函数pay指导money变量的存储位置-专属私密。
小结:
外层函数-怀着宝宝的妈妈;
内层函数-妈妈肚子里的宝宝
调用外层函数妈妈时:
总是会自动创建外层函数妈妈的作用域对象,其中保存着外层函数的局部变量例如:money=1000,就像妈妈给自己即将出生的宝宝包的红包。
小结:
定义一个外层函数妈妈相当于new Funcuitn(),创建一个临时的函数对象,
调用外层函数时,就会进行三部曲:
1.备料; 2.按照菜谱做菜; 3.清理厨房。
创建一个临时的函数作用域对象,保存着局部变量红包,按照程序执行,
里面return引用着被保护的局部变量(红包)的内层函数宝宝,
执行完销毁此临时作用域函数对象和变量,但因为内层函数的作用域链引用着外层函数的作用域对象,
所以外层函数的作用域对象无法被释放,从而形成了闭包。
提示:函数只有在调用时才会执行里面的程序。
内层函数被包裹在外层函数里面,所以,内层函数的作用域链是3个层;
- 内层函数作用域对象
- 外层函数作用域对象
- window作用域对象
程序继续往下走,此时,fun函数已经调用完毕
调用完毕之后,pay就有了赋值,那么有一个问题,fun和pay也就是妈妈和孩子还有关系吗?
答案是:没有关系了
结果:内层函数宝宝一降生,就得到了外层函数妈妈的专属红包,从此自立门户,与外层函数妈妈再无关系
内层函数自立门户后是如何执行的?
依旧是做菜三部曲
第一步:备料
创建一个临时的函数作用域对象,保存局部变量。
这时,函数pay就有了自己的作用域,并且有一个自己的地址1*3434。
第二步:按照菜谱做菜
此时会跟着js引擎向下执行程序,第一行代码出现变量money,会根据作用域链进行查找,先从自身找看是否有money变量,没有就继续向上找,自己的妈妈fun的作用域里面有money,就截止了,就不会继续向上找了。开始继续执行程序,由于妈妈的作用域里的money值是1000,这时通过计算,减掉100,此时money的值就被更改为900;
第三步:清理厨房
此时函数f地址为1*3434的pay函数已经执行完毕,
紧接着pay函数作用域对象伴随着它的局部变量一块进行销毁,只会销毁离fun最近的一个作用域,也就是它自己。
所有函数调用完只清空作用域链中离自己最近的一个作用域
继续执行程序
执行money=0;这行代码,因为该变量全局没有声明过,所以:从未生声明过得变量赋值,会自动在全局创建该变量,最后全局的money变量被赋值为:0;
继续执行下一行代码,又是调用内层函数
又是调用函数的三部曲;
每次调用,会临时生成新的函数作用域对象以及新的地址,然后找变量,从作用域链上找,先找自身的;
局部的变量money值被修改为800;函数执行完毕,就是最后一步啦,清理厨房。
最后一行代码还是调用内层函数;内部调用情况跟上面的一样;
最后局部的money=0并没有修改掉闭包中的money值;
到底什么是闭包?
- 闭包是个对象
- 闭包就是每次调用外层函数时,临时创建的函数作用域对象
- 为什么外层函数作用域对象能留下来?因为被内层的函数作用域引用着,无法释放
一句话概括闭包如何形成?
外层函数调用后,外层函数的作用域对象被返回的内层函数作用域对象引用着,无法释放,就形成了闭包对象。
结言
最后:
这篇总体来说把闭包说明白了,但是还有一小部分关于闭包的缺点,怎么解决这个缺点,以及做闭包题的法则答题技巧(ps:知道这个法则,基本闭包方面的题,都能轻松应对)没说,因为太晚了,这篇篇幅也比较长,想睡觉了。有兴趣的同学,看了这篇没有解渴的同学,找下篇或者等更呀。