图解JS闭包形成的原因

前言

什么是闭包,其实闭包是可以重用一个对象,又保护对象不被篡改的一种机制。什么是重用一个对象又保护其不被篡改呢?请看下面的详解。

作用域和作用域链

注意
理解作用域和作用域链对理解闭包有非常大的帮助,所以我们先说一下作用域和作用域链

什么是作用域
作用域表示的是一个变量的可用范围、其实它是一个保存变量的对象
为什么要使用作用域
避免不同范围的变量互相干扰

作用域包含了哪两种
1、全局作用域
在JavaScript中的全局作用域其实就是windows
优点:可重复使用,随处可用
缺点:会造成全局污染
clipboard.png

2、函数作用域
临时创建的活动对象AO(Activation Object)、该对象包含了函数的所有局部变量、命名参数、参数集合以及this,当运行时上下文被销毁、活动也会被销毁(闭包形成的原因其实因为就是因为活动对象被引用着无法被销毁而导致的,详细的请继续往下看)
优点:不污染全局
缺点:不可重复使用、仅在函数内可以使用

程序执行的四个阶段
我以下面一段代码解释一下程序执行的几个阶段

    var age = "21";
    function myAge(){
        var age = 0;
        age++;
        console.log(age);
    }
    myAge();
    console.log(age);

第一阶段:在内存中创建执行执行环境栈、把全局对象window压入栈底、在window中声明变量
图片描述

第二阶段:函数调用时
在执行环境中添加当前函数调用、为本次函数调用创建活动对象AO、根据scope指定运行期活动对象AO的上下文内部对象
图片描述

第三阶段:函数调用后
函数调用从执行环境栈中出栈、函数作用域AO释放、函数作用域AO中的局部变量也一同被释放
图片描述

由上面可以看出当一个函数调用完毕它的局部变量就会被释放,下次再次调用时会创建新的局部变量。这就是函数中的局部变量不可重用的原因。

为什么要使用闭包

先介绍一下全局变量和局部变量的优缺点
全局变量:可以重用、但是会造成全局污染而且容易被篡改
局部变量:仅函数内使用不会造成全局污染也不会被篡改、不可以重用
从上面可以看出全局变量和局部变量的优缺点刚好是相对的。闭包的出现正好结合了全局变量和局部变量的优点。

何时使用闭包
希望重用一个对象,又保护对象不被污染篡改时

闭包的实现原理

弄清楚了作用域和作用域链、闭包实现的原理也就很容易弄懂了。
下面请看一段代码和几张图^_^
这是一段闭包的代码,我们又这段代码讲讲闭包的原理

    function addAge(){
        var age = 21;
        return function(){
            age++;
            console.log(age);
        }
    }
    var clourse = addAge();
    clourse();
    clourse();
    clourse();

第一阶段:在内存中创建执行执行环境栈、把全局对象window压入栈底、在window中声明变量(和前面是相似的)
图片描述

第二阶段:
1、在栈中添加addAge的函数调用
2、为addAge函数创建活动对象AO、根据addAge函数的scope可以知道其活动对象指向window
3、window对象中的clourse变量记录着addAge()返回的匿名函数的地址[现在addAge()和clourse变量都可以找到匿名函数和addAge()产生的AO]
图片描述

第三阶段:
addAge()调用完毕出栈、其对活动对象AO的引用也随之消失。
在作用域和作用域链中举的例子中,活动对象AO会被JS中的垃圾回收机制回收大家还记得嘛^_^,
但是这里和前面是不一样的哦!注意了:匿名函数中的scope引用着活动对象AO、匿名函数的地址也被clourse变量记录着。因此,addAge()虽然出栈了,对它的活动对象的引用也消失了,但是其活动对象被匿名函数的scope拽着、所以无法释放不会被回收。
大家观看蓝色的箭头,其实可以发现、蓝色的箭头已经形成了一个闭环了。
此时,由图也可以看出,活动对象AO只能通过clourse变量来找到。这里形成了一个闭包。保存了addAge()函数中的局部变量,使其可以重复使用,但是又不会造成全局污染。这就是闭包的一个使用场景:保存现场。
至于怎么调用重复使用局部变量,具体过程请看下面两幅图。
图片描述

第四阶段:
clourse()进栈,产生clouse()的活动对象AO,根据它的scope可以知道它的__parent__指向addAge()产生的活动对象AO。
clouse()执行age++,由于在它自己的作用域里面没有age、于是它会到上一级作用域查找age,它在它的上一级作用域中找到了age,于是对其进行了age++,age从21变成了22。执行console.log(age)输出22。
图片描述

第五阶段:
clourse()出栈,因为clourse产生的AO没有scope拽着它,因此clourse的AO是可以正常释放的。函数出栈,其AO被JS的垃圾回收机制回收。
clourse变量中的匿名函数中的scope依旧拽着addAge()产生的活动对象AO,于是这个活动对象依旧无法被释放[而且这个AO现在只能被clourse找到、clourse可以重复使用这个AO里面的局部变量age、又不会造成全局污染]
图片描述

剩下阶段:
代码中还有两个函数还有执行

clourse()
clourse()

它的执行和第四阶段和第五阶段的原理是一样的。所以在这里我就不重复画图了。
执行clourse():看第四阶段的图,age从22变成了23,执行console.log(age)输出23、看第五阶段的图
执行clourse(): 看第四阶段的图,age从23变成了24,执行console.log(age)输出24、看第五阶段的图

总结

以上就是对闭包形成过程的图解。也说明了闭包保存现场的作用场景。闭包结合了局部变量和全局变量的优点。可以使变量不污染全局,但是又能对变量进行重用。但是,其实闭包也有有缺点的,它比起普通函数会占用更多的内存。
总的来说,以上就是我对闭包的理解。如果大家发现了什么错误欢迎评论指出,一起交流一起学习一起进步!^_^

阅读 5.6k

推荐阅读