你不知道的闭包原理【三个栗子彻底理解】

你不知道的闭包原理

想要理解闭包之前,就必须理解函数的创建过程、活动变量AO、作用域链。我曾写过相关的文章

网上相关对闭包的定义:

  • MDN:函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。
  • 你不知道的JavaScript:是指有权访问另外一个函数作用域中的变量的函数。创建闭包的常见方式就是在一个函数内部创建另外一个函数。
  • Javascript核心技术开发解密:闭包是一种特殊对象,由两部分组成:执行上下文A + 该执行上下文创建的函数B

我对这些定义的理解:

  • 《MDN》的解释更加接近原理,
  • 《你不知道的Javascript》的解释更多讲的是现象,
  • 《Javascript核心技术开发解密》的解释更能说明闭包的真实存在:闭包是一种特殊对象。

———————————人工分割线——————————

下面我们通过几个栗子来一步步讲解闭包的原理:

栗子一:

    function makeFunc() {
        var name = "Mozilla";
        function displayName() {
            console.log(name);
        }
        return displayName;
    }

    var myFunc = makeFunc();
    myFunc();

打印结果:Mozilla

  • 混淆点1当执行到var myFunc = makeFunc();makeFunc函数在执行完之后里面的name不是应该被垃圾回收机制给处理掉了吗?
    答案:其实通过垃圾回收机制大概也知道name属性不可能被回收,因为还有myFunc函数持有name的引用。
  • 混淆点2:与普通函数有什么不同?
    答案:如果没产生闭包,那么函数中的临时变量都被回收了。
  • 混淆点3:name属性如果不回收,那么存放在哪里?
    答案:这个问题后面会讲到
    【myFunc在google浏览器的内部属性,生成了一个闭包对象makeFunc】
    在这里插入图片描述

先来思考一个问题:我们知道当执行了makeFunc函数后会产生对应的变量对象{变量对象保存了函数中的临时变量},那么上图中的闭包对象makeFunc是否就等于makeFunc函数所产生的变量对象?

带着这个问题看下一个栗子–

栗子二:

    function makeFunc() {
        var name = "Mozilla";
        var age = 12;
        function displayName() {
            console.log(name);
        }
        return displayName;
    }

    var myFunc = makeFunc();
    myFunc();

思考: 此时的闭包对象makeFunc是否带有age属性?
答案:没有age属性
在这里插入图片描述
说明:闭包对象不等于makeFunc的变量对象。闭包对象仅保存跨域的属性。

延申到另一个常见问题:如何清除闭包?
我们知道闭包对象存在于myFunc函数内,所以一句:myFunc = null。使得闭包对象没有引用持有那么等待他的就是垃圾回收。

再延申到另一个常见问题:MDN:在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。那么岂不是内存很快就泄露了?
实际上你的闭包大多数都是没有引用持有,很快就会被回收掉的。并且JS对闭包也有相关的优化处理。

然而:这个时候,我们是否明白这个闭包对象与作用域链的关系是什么?

栗子三:

	var a = 20;
    function test () {
        var b = a + 10;
        function innerTest () {
        	debugger
            var c = 10;
            return b + c;
        }
        innerTest();
    }
    test();

??? 当执行到debugger时,此时innerTest函数的作用链是什么呢?闭包对象是否产生?
:::此时innerTest函数的作用链:
在这里插入图片描述
闭包对象已经产生,并且闭包对象作为作用域链中的对象。

你是否记得很多书上都说作用域链是一条又每个函数的VO对象组成的链条。但这里看到的却不是VO对象,而是闭包对象。

我的看法是:如果单纯从函数的作用域来看:作用域链是一条又每个函数的VO对象组成的链条。这个说法很正确,这是真正能够以此帮助我们判断访问作用域边界的依据。但是在程序实际的运行中,经过词法编译的阶段,JS引擎已经通过代码把各个实际上闭包产生的变量已经提炼出来。而不是直接就把VO对象放在作用域链。这也有利于提高访问速度。

总结:

    function makeFunc() {
        var name = "Mozilla";
        function displayName() {
            console.log(name);
        }
        return displayName;
    }

    var myFunc = makeFunc();
    myFunc();

执行过程有关闭包的变化: 当调用makeFunc()函数进入函数创建阶段时发现displayName函数含有name的跨函数变量,所以在对displayName函数进行提升的时候就已经给displayName函数初始化了闭包对象【makeFunc闭包】。所以当执行myFunc()函数的时候,从当前myFunc()的VO对象找不到的话就会从作用域链中的上一级【makeFunc闭包】对象中找。
来个图清晰一点:
在这里插入图片描述
— 以上便是对闭包最新的理解。不对的望多多指出。