JS闭包的由浅入深理解和实例分析

网上有各种对闭包的比较靠谱的解释,这里说主流的两种:

1. 闭包是能读取其他函数内部变量的函数,本质上是函数内部和函数外部连接的桥梁

2. 函数和其周围状态(词法环境)的引用捆绑在一起,形成闭包

这里第一个说法相对比较好理解,但是第二个说法会比较晦涩,需要有一定的理论基础。

在做具体的分析之前,有几个概念需要提前弄明白:作用域,作用域链,执行上下文,环境变量。最好也能够明白执行栈任务队列,容易放在一起考察。

环境变量:

全局变量和局部变量,分别声明在全局作用域和函数/块作用域中。

全局变量是任何地方都能访问的变量。声明全局变量代价很高,一是因为全局变量声明周期很长,二是因为变量暴露在外面很容易被修改。所以为了尽可能的减少全局变量在代码中的应用,闭包便可以用来让存储这些原本危险的变量,这点在之后的分析中说明。

局部变量储存在函数内部,或者块内部,无法被其作用域外部访问到。相对来说很安全。

作用域:

作用域有两种工作模型,词法作用域(静态)和动态作用域。JS采用的是词法作用域,意义为,函数的作用域在函数声明时便决定了。JS中主要是全局作用域和函数作用域,现在还有块作用域,ES6中的let和const声明就是块作用域的体现。

词法作用域的理解:

let a = 1;
function print(){
    console.log(a);
}
function doPrint(){
    let a = 2;
    print();
}
doPrint(); // 1

在运行这段代码的时候,具体过程如下:

1. 首先一个变量和两个函数分别被放进内存中,作为全局变量和全局函数,然后执行doPrint函数

2. doPrint内部形成局部作用域,赋值a = 2,执行print函数

3. print函数运行,发出让控制台打印a的命令,然而print内的局部作用域并没有a,怎么办呢?需要向外查找。根据词法作用域的规则,我们应该忽略掉print运行时的作用域,转而去查找定义print函数时的作用域,向外查找,便找到全局变量a = 1,执行打印,得到结果为1

作用域链:

我们可以通过查找变量的过程来理解作用域链。像上面的例子,在print函数中的a变量,是在当前的上下文的变量对象中找不到的,此时会在父级的执行上下文中继续查找变量,这里正好是全局对象,也是作用域链的顶端。这种由多个执行上下文的变量对象形成的链表就叫做作用域链。

作用域链的特点:

1. 外部的作用域不可访问内部作用域中的变量

2. 内部的作用域可以访问作用域链上外部作用域中的变量

执行上下文:

当函数执行时,在执行栈中创建一个叫做执行上下文的内部对象,定义函数执行时的环境。

类型:全局执行上下文,函数执行上下文,eval函数执行上下文(较少见)

执行栈:

模型类似于数据结构中的栈结构,用于存储代码执行期间所有的执行上下文,首次运行JS代码时,就会创建一个全局执行上下文,并push到执行栈中,每当有函数运行的时候,便将这个函数执行上下文push到执行栈,如果函数中还有另外一个函数运行,那么以此类推。直到最内部的函数执行完毕,最内部的那个函数执行上下文pop弹出,返回上一层的执行上下文,继续运行代码,运行完毕后,当前的执行上下文继续弹出,以此类推,直到返回至全局上下文。

这里举个例子:

let a = 1;
function doPrint(){
    let a = 2;
    print();
    console.log(a)
}
function print(){
    console.log(a);
}
doPrint();

这里稍微对上面的例子进行一点修改,你觉得结果会输出什么呢?

下面是分析:

1. 先执行doPrint函数,将当前的函数执行上下文push到执行栈中,里面有个局部变量a = 2,保存到变量对象中。

2. 执行print函数,将这个函数的执行上下文push到执行栈中,然后打印a,寻找变量a,因为a不在当前的函数作用域中,所以我们也叫它自由变量,因此根据作用域链的关系,会向外查找,也就是全局作用域中,发现了全局变量a = 1,因此我们第一个输出的是1。

3. print函数执行完毕,当前执行上下文弹出栈,回到doPrint的函数执行上下文,继续执行,也是一个打印指令,输出a,寻找变量a,这次比较幸运,当前的函数作用域中就有a变量,所以第二个输出的是2。

4. doPrint函数执行完毕,当前执行上下文出栈,在全局执行上下文中往下继续执行代码。

所以结果就是先输出1,再输出2,怎么样,你答对了吗?

闭包:

最后来重新看闭包,梳理一下刚才的概念,回到最初说的两个主流的定义:

闭包是能读取其他函数内部变量的函数,本质上是函数内部和函数外部连接的桥梁

我们看一个新例子:

let foo = function() {
    const a = 1;
    let bar = function() {
        console.log(a);
    }
    return bar;
}

let res = foo();
res();

这里我们先定义了一个函数foo,函数内部定义了一个变量a和一个函数bar,返回函数bar。那么运行foo这个函数的结果,实际上返回的是一个函数,那res这里也是一个函数,再运行res,这样的一个过程。

然后我们分析一下这个运行过程:

1. 全局上下文中foo和res都存放在堆内存中(假设地址为AAA和BBB),变量对象(VO)中仅存放引用地址。

1.  运行res()函数,发现需要先运行foo()函数,开启foo函数的执行上下文(注意这里有两个括号是因为foo函数本身返回的也是一个函数,所以还需要一个括号运行)。

2. 开启foo的执行上下文之后,得到一个私有变量a = 1,和一个函数bar存放在另外一个堆内存中(假设地址为CCC),储存在新的活动对象(AO)中,代码继续执行,return bar这个函数,函数执行完毕。可是此时我们能弹出当前的这个执行上下文吗?我们发现,return bar的这个结果,实际上是foo(),也就是函数res的位置也存放在CCC这个地址。所以foo这个上下文中的部分内容(bar的堆内存地址是CCC)被外部的全局执行上下文占用(res占用),因此无法弹出,形成了一个闭包(closure)。

3. 然后在全局上下文中运行的foo()()现在就变成了bar(),bar这个函数,本身在全局上下文中是没有的,全局上下文中只有foo和res的引用地址,但由于res的引用地址是引用闭包里面的bar,因此在全局上下文中的代码执行,是有权访问foo的执行上下文中的内容,也就是bar这个函数。

4. 运行bar函数,打印a,当前作用域找不到,向外,找到了foo里的a = 1,于是输出1。

完整的分析过程就结束了。我们看到这个例子中,外部有权访问内部变量,也就是桥梁。

接着我们看闭包的另外一个主流说法

函数和其周围状态(词法环境)的引用捆绑在一起,形成闭包

虽然还是听起来非常抽象,但是经过第一个例子的解释,更容易理解了。这里的函数就是例子里的bar函数。且bar函数和其词法环境的引用捆绑(res的引用地址最后指向的是bar的引用地址),形成闭包,并导致bar所在的执行上下文无法弹出,可以通过外面的res函数访问。

闭包的作用与问题:

所以闭包到底有什么用呢?函数内部与外部的桥梁,到底有什么用呢?

闭包的主要应用是避免全局变量的污染,也是之前在环境变量中提到的问题。多人开发中,为了避免全局变量重复,所以我们会尽量利用闭包,来将一些全局变量私有化,去尽可能的减少全局变量的使用。

闭包的问题在于,无法弹出的执行上下文,会一直占用内存,因此,滥用闭包的话会非常占用空间。解决方案是,在退出函数时,将不必要的变量全部删除,设置为null。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值