Javascript闭包学习

609 阅读5分钟

闭包是什么?

当内部函数被保存到外部时,将会产生闭包。

先来一段代码!

function test() {
	var count = 0
    count ++
    console.log(count)
}
test()
test()

此时调用两次 test( ) 打印出的都是 1

那么如果我们要实现计数器的效果(打印出1,2)应该怎么做?

最简单的方法是将 var count = 0 放入全局中,代码如下:

function test() {
    count ++
    console.log(count)
}
test()
test()

此时打印出的是 1, 2;实现了计数器的效果。

但在模块化开发时,我们应该尽可能避免声明全局变量,这个时候就能发挥 闭包 的作用了。

下面来看看怎么利用闭包来实现计数器的功能吧~

function test() {
	var count = 0
    function add() {
    	count ++
        console.log(count)
    }
    return add
}
var result = test()
result()
result()

此时打印出来的两个数也是 1,2

闭包的原理

那么闭包的原理是什么呢?用图片来解释会方便很多

  • 首先我们要了解在函数执行前的 预编译 的简单过程:

    在函数执行前有一个 预编译 的操作,预编译的过程在这里就不多说了,只要知道预编译时会在全局(window)中创建一个 GO对象(Global Object), 在函数体内部创建该函数体的 AO对象(Activation Object) 就行~

    下图展示的是预编译结束后产生的 GO对象和 AO对象,为了更好理解,我把代码也贴到图上

  • 函数执行

    在正常的执行过程中,函数执行完后其对应的AO对象会被删除(清除指向AO对象的指针),但此时 test() 中的 add() 被返回出去,使得 result() 拥有了一个与 add() 相同的AO对象。
    对于 result() 来说,他的作用域链是这样的: 在 result() 开始执行后,需要执行 count++ 这一操作,那么他会先在自己的作用域中寻找 count 这一变量,没有找到则去到下一个作用域-- test() 的作用域中寻找,得到了 count 后执行,此时 count == 1 且依然存储在 test() 的 AO对象中,由于此时 result() 已经执行完,指向它的 AO对象的指针将会被删除,此时引擎中的 GO对象和 AO对象有:

    下一个 result() 执行的时候会发生类似的事情,但js引擎将会创造一个新的 AO对象,让指针指向它。对于这个 result() 来说,它的作用域链和上一个的结构是一样的,不一样的地方在于它的 AO对象和上一个 result() 的 AO对象存储于不同的地址中。所以它取到的 count 也是存储在 test() 中的 count, 此时count == 1, 所以打印出来的是 2

  • 为什么 test() 的 AO对象不会在内存中被删除呢?

    在test()的执行过程中, 将其内部的 add() 返回给了外部变量result,也就是说,在 test() 的外部,一直有一个指针指向其内部存储的 add() 函数。
    如果 test() 的 AO对象被删除,那么存储在其中的 add() 也会被删除,但外部变量的指针依然要指向 add()
    为了保证外部变量 result 的正常使用,js引擎并不会删除 test() 的 AO对象,也就让 count 这一变量顺利保存在 test() 的 AO对象中了

闭包的作用

从以上代码中我们可以简单归纳一下闭包的几个作用:

  1. 实现公有变量,这一点在实现企业的模块化开发中很重要
  2. 拿上面写到的代码为例,count 是被保存在了 js 的引擎中,所以可以实现累加的效果 => 闭包可以帮助我们实现缓存的功能
  3. 防止全局变量污染全局,这个不多解释啦

循环中的闭包

最后来看一个有点难的代码

function test() {
    var arr = []
    for (var i = 0; i < 10; i++) {
        (function (j) {
            arr[j] = function () {
                console.log(j)
            }
        })(i)
    }
    return arr
}

var myArr = test()
for (let i = 0; i < 10; i++) {
    myArr[i]()
}

这里打印出来的是什么?


结果是 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
先思考一下再来解释!


首先,以下代码和上面的代码的 test() 函数功能完全相同,只是把自执行函数改成了普通的函数声明并调用

function test() {
    var arr = []
    for (var i = 0; i < 10; i++) {
        function addFunc(j) {
            arr[j] = function () {
                console.log(j)
            }
        }
        addFunc(i)
    }
    return arr
}

这样是不是视觉上好理解一些

我们来理解一下 test() 中的 for循环都做了些什么:

  1. addFunc() 会接收一个名为 j 的形参,当 for循环到第i个时,j接收的值为 i ,创建自己的 AO对象
  2. 接下去运行, arr[j] 会被赋值为一个匿名函数 function () { console.log(j) } ,此时这个匿名函数的 AO对象中并不存在 j 这个变量,所以它会向父级作用域中寻找 j,j 在 addFunc() 的 AO对象中被找到
  3. addFunc() 执行完毕,按理来说 addFunc() 的 AO对象应该被删除,但由于 arr[j] = function () {...} 是在其内部声明的并被返回了出去,也就是说,每当 arr[i] 被调用时,都需要回到 addFunc()的 AO对象中来找 arr[i] 这个函数体的具体内容,所以 addFunc() 的 AO对象被保存了下来(此时传入的j值为i)
  4. 下一步执行的是addFunc(i + 1),它又会创建一个新的 AO对象,重复第2、3步,但其 AO对象中保存下来的 j 值为 i + 1

在完成for循环后,每个 arr[i] 对应的函数体都留有一个父级的 AO对象,在这个父级的 AO对象中存储了他们需要打印的 j值

将 test() 中的 arr 返回到函数外部,赋值给 myArr,test() 的 AO对象也因此存储在js引擎中不被删除

所以在最终调用时,myArr[num] == function () { console.log(j) } //为了防止歧义这里使用num来指代一个具体的数字
在这个函数体的父级 AO对象中,存储了它需要打印出的 j值

所以!最后打印出的结果其实是每一次传递进去且被保存下来的 0-9


完结撒花!如果有我没有解释清楚的地方可以评论告诉我!我都会尽力补上的!
如果我有可以改进的地方(比如用词啊阐述啊什么的)也可以评论告诉我!
如果没有 欢迎评论闲聊 (夸我也行) 谢谢!!!