闭包(上)

206 阅读11分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

前言

面试时总被问到闭包?内心没有概念答不出来?面试前看了一些概念性的总结文章或者背点经典的闭包笔试题,但对底层的原理还是不清楚,面试官针对闭包问题再次深挖或者扩展,就懵逼?来来来,看这里,保证一文彻底搞懂闭包,再他闭包笔试题千变万化,也能轻松应对。
其实平常开发中已经用了很多了,但是自己却不知道那其实就是闭包。

闭包

前奏

开始之前,我们先看一下下面这个栗子:
如果让你写一段代码,小孩过年拿到1000块钱红包,每次花出去100,这个程序怎么写?
同学们肯定说那简直不要太easy,于是乎,出现了下面一顿猛操作

 var money=1000;
 function fun (){
   money-=100;
   console.log(money);
 }
 fun();
 fun();

image.png 执行了两次,也就是每次花了100块钱,看控制台最后剩了800,这段代码没有错。但是有个问题?如果别的开发小伙伴也用了这个money变量了呢,看看发生了什么?

var money=1000;
function fun (){
   money-=100;
   console.log(money);
}
fun();
money=0;
fun();

image.png 小孩哭了,到手的钱还没怎么花呢,就花了100,怎么还欠银行100呢,对呀,这值怎么就成负数了呢,怎么就-100了呢,明明是800才对呀?
这是因为money是全局变量,随时随地谁都可以用,容易被篡改,容易被污染,如果开发团队中,一个人定义了全局变量,开发中定义的变量太多了,本人都不知道自己定义了什么变量,那就容易被其他成员使用篡改,因此,开发团队中,一般不建议使用全局变量
有的小伙伴,一想,全局容易被污染,那我定义成局部的不就完事了吗,真的行的通吗?
往下看->

function fun (){
    var money=1000;
    money-=100;
    console.log(money);
}
fun();
money=0;
fun();

运行结果是这样的: image.png 这回换银行该哭了,钱怎么取怎么花剩的钱都是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();

看看效果吧:

image.png 是不是完美解决了,这就是闭包了,平常开发中挺常用的吧,用的可能比这复杂的多,这里就是举个栗子,那么闭包到底是什么呢?往下看哦

什么是闭包?

既可以重复使用一个变量,又不会被污染的一种编程方式

什么时候使用?

希望给一个函数,保存一个既可以重复使用,又不会被外界污染的的专属的局部变量时,就使用闭包。

怎么使用?

  1. 外层函数包裹保护的变量和使用此变量的一个内层函数;
  2. 外层函数内部返回内层函数;
  3. 外部调用外层函数并接住返回的内层函数;
//第一步:外层函数包裹被保护的变量和使用此变量的内层函数
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被重复使用了。

为什么要这样使用?

image.png 定义外层函数时,小写fun会被自动翻译为:new Function();

image.png 这时候fun也就会有一个自己的地址,这个地址存着fun函数对象,同时形成了一个作用域链对象,
里面目前有两个作用域,一个是window,另外一个是fun自己的作用域对象,
只不过还没有调用执行fun,所以目前还是空的。

image.png 开始执行var pay=fun()这行代码,全局作用域里面多了一个pay全局变量,
此时pay值为undefined,因为此行代码还没有执行完。

image.png 调用函数fun,此时执行调用函数三部曲第一步:备料,(详情可查看上一篇文章,作用域&作用域链 https://juejin.cn/post/7067564413342973966
创建一个临时的函数作用域对象,保存局部变量。
这时,函数fun就有了自己的作用域,并且有一个自己的地址1*5656。

image.png 继续执行fun函数,这里开始三部曲的第二步:按照菜谱做菜(根据js引擎执行程序)
这时局部变量money已经被赋值1000,执行到定义一个fun时,自动创建一个新的函数对象,并有自己的地址1*8585,new Function();
最后此内层函数被返回。
此时的内层函数的作用域链上有3级作用域(?,因为:在定义一个函数时,就已经形成了自己的作用域,详情可查看上一篇文章,作用域&作用域链 https://juejin.cn/post/7067564413342973966 ),自己(空),fun(妈妈),window。
因为内层函数并没有被调用,所以,并没有创建自己的临时作用域,因此自己的作用域为空。
还有就是,闭包结构的内层函数比一般函数多了一级作用域,多的那一级是外层功函数也就是自己的妈妈。

耳听为虚,眼见为证: image.png console.log.dir可以打印出对象的作用域链,可以看出除了自己,里面有两个数据,分别是fun和Global。

image.png 这里开始三部曲的第三步:清理厨房, 这里可以看到,pay作用域链里有一级是指向外层函数fun的作用域对象的,
此时函数fun已经执行完毕,紧接着fun函数作用域对象伴随着它的局部变量一块进行销毁,只会销毁离fun最近的一个作用域,也就是它自己。
但是这里,作用域对象并没有被销毁,这是为什么呢?
这是因为:
pay变量也就是内层函数的作用域链里面引用着外层函数fun的作用域对象,因此,外层函数fun的作用域对象无法释放,从而形成了闭包
而且只有内层函数pay指导money变量的存储位置-专属私密

小结:
外层函数-怀着宝宝的妈妈
内层函数-妈妈肚子里的宝宝

image.png
调用外层函数妈妈时:
总是会自动创建外层函数妈妈的作用域对象,其中保存着外层函数的局部变量例如:money=1000,就像妈妈给自己即将出生的宝宝包的红包。

image.png 小结:
定义一个外层函数妈妈相当于new Funcuitn(),创建一个临时的函数对象,
调用外层函数时,就会进行三部曲:
1.备料; 2.按照菜谱做菜; 3.清理厨房。
创建一个临时的函数作用域对象,保存着局部变量红包,按照程序执行,
里面return引用着被保护的局部变量(红包)的内层函数宝宝,
执行完销毁此临时作用域函数对象和变量,但因为内层函数的作用域链引用着外层函数的作用域对象,
所以外层函数的作用域对象无法被释放,从而形成了闭包。

提示:函数只有在调用时才会执行里面的程序。
内层函数被包裹在外层函数里面,所以,内层函数的作用域链是3个层;

  1. 内层函数作用域对象
  2. 外层函数作用域对象
  3. window作用域对象
    程序继续往下走,此时,fun函数已经调用完毕
    image.png 调用完毕之后,pay就有了赋值,那么有一个问题,fun和pay也就是妈妈和孩子还有关系吗?
    答案是:没有关系了
    结果:内层函数宝宝一降生,就得到了外层函数妈妈的专属红包,从此自立门户,与外层函数妈妈再无关系
    内层函数自立门户后是如何执行的?
    依旧是做菜三部曲
    第一步:备料
    image.png 创建一个临时的函数作用域对象,保存局部变量。
    这时,函数pay就有了自己的作用域,并且有一个自己的地址1*3434。

第二步:按照菜谱做菜 image.png
此时会跟着js引擎向下执行程序,第一行代码出现变量money,会根据作用域链进行查找,先从自身找看是否有money变量,没有就继续向上找,自己的妈妈fun的作用域里面有money,就截止了,就不会继续向上找了。开始继续执行程序,由于妈妈的作用域里的money值是1000,这时通过计算,减掉100,此时money的值就被更改为900;
第三步:清理厨房
image.png
此时函数f地址为1*3434的pay函数已经执行完毕,
紧接着pay函数作用域对象伴随着它的局部变量一块进行销毁,只会销毁离fun最近的一个作用域,也就是它自己。
所有函数调用完只清空作用域链中离自己最近的一个作用域
继续执行程序
image.png

执行money=0;这行代码,因为该变量全局没有声明过,所以:从未生声明过得变量赋值,会自动在全局创建该变量,最后全局的money变量被赋值为:0;
继续执行下一行代码,又是调用内层函数
image.png

又是调用函数的三部曲;
每次调用,会临时生成新的函数作用域对象以及新的地址,然后找变量,从作用域链上找,先找自身的;
image.png

局部的变量money值被修改为800;函数执行完毕,就是最后一步啦,清理厨房。
最后一行代码还是调用内层函数;内部调用情况跟上面的一样;
最后局部的money=0并没有修改掉闭包中的money值;

到底什么是闭包?

  • 闭包是个对象
  • 闭包就是每次调用外层函数时,临时创建的函数作用域对象
  • 为什么外层函数作用域对象能留下来?因为被内层的函数作用域引用着,无法释放

一句话概括闭包如何形成?
外层函数调用后,外层函数的作用域对象被返回的内层函数作用域对象引用着,无法释放,就形成了闭包对象。

结言

最后:
这篇总体来说把闭包说明白了,但是还有一小部分关于闭包的缺点,怎么解决这个缺点,以及做闭包题的法则答题技巧(ps:知道这个法则,基本闭包方面的题,都能轻松应对)没说,因为太晚了,这篇篇幅也比较长,想睡觉了。有兴趣的同学,看了这篇没有解渴的同学,找下篇或者等更呀。