浅析JS闭包 —— 与闭包的初次邂逅

写在前面

第一次听闭包的时候,还是在刚决定学习前端的时候我同学加学习好伙伴告诉我的,那个时候感觉好高级啊,听不懂,懵懵的。在学习html和css时也会看到一些技术文章。在看的过程中也会碰到一些比较深入的文章,关于js的内容,我也浏览过写js闭包的文章,也是很认真的硬着头皮看了看。但是呢,发现还是一头雾水,读不懂它的含义,依然是感觉闭包到底是个什么东西,什么叫外部函数可以访问其他函数内部的变量。现在是理解了,豁然开朗,柳暗花明,脑袋嗡的一下明镜了。哈哈哈哈!

补充一点,如果把函数的作用域链的概念搞得很清楚,透透的话,你会发现闭包超级好理解,难的是到后面我们需要用闭包解决闭包的问题。我在前面两篇文章都做了详细的介绍,可以看一下

预编译:理解作用域的本质  https://blog.csdn.net/qq_42383764/article/details/105229455

作用域链:https://blog.csdn.net/qq_42383764/article/details/105242677

 

闭包的概念

emmmm,其实是不太能记住闭包的官方概念,个人简单理解,当内部函数保存到外部时,就形成了闭包。 闭包会导致原作用域链不释放,造成内存泄漏。翻译过来就是,外部作用域是可以访问到内部函数里的数据。

我们都知道,函数只有在被执行时,才会产生自己的执行期上下文也就是作用域,当函数执行完之后,这个函数是会被立即销毁的,更好理解的说法是释放空间。闭包就是函数执行完毕后,本该销毁的,但是内部函数被保存出来了没有销毁掉,这就是闭包。

还是不太明白怎么回事对不对?我们需要知道的是为什么会这样,它是怎么实现的,我们要知其然、也要知其所以然。我来举个例子,分析分析执行过程,就可以解决这些问题了。

 

补充知识

在举例之前,这里需要补充一个知识点,看下代码

        function a() {
            // b denfined 时 b.[[scope]] -- > 0:AO 对象
            //                             1:GO 对象
            function b() {
                var bb = 234;
            }
            var aa = 123;
            // b doing 时 b.[[scope]] -- > 0:bAO 对象
            //                             1:aAO 对象
            //                             2:GO 对象
            b();
        }
        var glob = 100;
        // 调用a函数,在a函数产生一个执行期上下文 创建a的AO对象 并把自己产生的AO对象放到作用域的最顶端
        // a doing a.[[scope]] -- > 0:AO 对象
        //                          1:GO 对象
        a();

 

主要的地方就是,要注意在a函数执行时,b函数是站在a函数的肩膀上的,所以b函数在定义时,b函数的作用域链中就有a函数的作用域链的内容,当b函数再执行时,b函数会产生它自己的执行期上下文(作用域——AO对象),并把它放到b函数作用域链的最顶端 。再详细的介绍可以去看我的另一篇文章—— 作用域链:https://blog.csdn.net/qq_42383764/article/details/105242677。这里不再赘述。

 

 

闭包案例分析

代码,思考一下运行结果。

        function test() {
            var aa = 11;
            function b() {
                console.log(aa);
            }
            return b;
        }
        var num = test();
        num();

上面的例子运行结果是什么? 答案就是 11 。

这是一个最最最简单本且基础的闭包。这里的b函数就是闭包。我们要注意的是 return语句 返回的是函数b的引用,也就是说函数a的返回值是函数b,当执行函数a时,用一个变量接收函数a的结果,再去执行时,执行的就是函数b。

函数a执行完毕的标志就是函数b别返回到全局作用域时,a函数执行完毕,立即销毁函数a。函数a自身是销毁了自身的执行期上下文,但是,因为函数b被返回到外部了,函数b的作用域链中还存储的有函数a的执行期上下文(AO对象)。函数a自身销毁AO对象并不影响函数b的作用域链。所以在函数b(num)执行时,仍然是可以访问到a函数的变量aa并且输出。

是不是还是懵懵的?这里要结合上面我补充的那一个知识点,当b函数定义时,b函数的作用域链与a函数的作用域链的内容是一样,所以在指向时,可以指向同一个对象。就是这个图

 

这个过程很清晰了吧,就是函数test销毁了它的执行期上下文,只是把test.[[ scope ]] 中的第0位与test的AO对象不再对应,test函数的AO对象任然存在函数b的作用域链中。所以,当num执行时,b函数会从作用域链的最顶端查询变量,可以找到变量aa,输入结果。

 

终于完结了,这就是我第一次学习到的js中的闭包,分享给大家啊。一定一定要明白作用域链形成的过程,以及函数执行完毕时,销毁函数的过程。

 

 

立即函数在闭包中的使用

立即函数我在做过详细的介绍,参考我的文章:https://blog.csdn.net/qq_42383764/article/details/105255544

在了解了闭包之后,是不是有豁然开朗的感觉,emmmmmm,也可能是我的错觉,但是目前我接触的知识,js闭包就是这样的,深入理解,扎根基础,哈哈哈哈哈。

来看个问题吧。你感觉会输出什么内容类?小心踩坑啊。

        function test() {
            var arr = [];
            for (var i = 0; i < 10; i++) {
                arr[i] = function () {
                    console.log(i + "");
                }
            }
            return arr; // 返回一个函数
        }
        
        var myArr = test(); // 把返回的函数function 引用给myArr
        for (var j = 0; j < 10; j++) {
            myArr[j](); 
        }

先给你看结果。

惊不惊悚?意不意外?刺不刺激?头不头疼?怎么回事?我是谁?我在哪?我在干些什么?

哈哈哈哈哈哈,上面的独白就是我看到结果时候的反应。当时我很想当然的认为输出的是 0 ~ 9这十个数,答案是直接从脑子里蹦出来的,但是仔细想想的时候,感觉不对,但是就是不知道哪有问题(还是我比较菜)。 

先来解释一下吧,主要是两点。

arr[ i ]这一句,只是一个函数表达式,它并没有执行后面的函数。数组arr[ ] 接收函数作为数组元素。所以,在执行函数test时只是把函数b返回到外部了,并没有进行其他任何操作。

第一:为什么是10的原因?—— 当 i = 10 时跳出循环,循环结束,此时 i = 10;意思就是说函数test的执行期上下文中AO对象中的变量 i 的值 更新为 10,并保存到函数b的作用域链中。所以当myArr[ j ]() 才是去执行函数b,函数b会产生自己的执行期上下文,并挂到作用域链的最顶端,函数b会从作用域链最顶端找变量 i 输出。此时的 i 是一直等于 10 ,不管你执行多少次函数b,i 一直是 10。

第二:为什么每次输出的结果一样?—— 因为,十次操作的是同一个test函数的AO对象,这就是 10 对 1的关系,当然就不能输出理想结果了。一对一的关系才会输出理想结果。

            for (var i = 0; i < 10; i++) {
                arr[i] = function () {
                    console.log(i + "");
                }
            }
            var myArr = test(); // 把返回的函数function 引用给myArr
            for (var j = 0; j < 10; j++) {
            myArr[j](); 
            }

 

综上所述吧,我们需要解决的问题是,让每一次循环都能找到对应的test函数的AO对象,而不是十次循环对应的是同一个test函数的AO对象。这就要使用立即执行函数了。

先看下解决方案。

解决方案就是使用立即执行函数,把 i 作为即时的参数,传给实参,这样就记录了索引号。它的本质就相当于放了十个立即执行函数,用谁调用谁。

分析一下过程:

当 i=0时,传参,立即执行函数实参 0,产生对应的立即执行函数。此时的0号立即执行函数中保存的test函数AO对象中i的值是 0 ,然后立即执行函数自己销毁自己AO对象。因为被保存到了外部,值也就保存了

当 i=1时,传参,立即执行函数实参 1,产生对应的立即执行函数。此时的1号立即执行函数中保存的test函数AO对象中i的值是 1 ,然后立即执行函数自己销毁自己AO对象。

以此类推,直到循环完十次,返回的是arr数组,里面装的是10个立即执行函数,但是每个立即执行函数的作用域链都不同,对应的是10个不同的test函数的AO对象。这样就达到了输出 0 ~ 9的理想结果。

        function test() {
            var arr = [];
            for (var i = 0; i < 10; i++) {
                // 把i作为实参传入立即执行函数
                // j是形参
                (function (j) {
                    arr[j] = function () {
                        console.log(j + "");
                    }
                }(i));
            }
            return arr; // 返回一个函数
        }
        
        var myArr = test(); // 把返回的函数function 引用给myArr
        for (var j = 0; j < 10; j++) {
            myArr[j](); 
        }

你再看结果,正确了吧。

这样的话,我也可以不用都输出,用谁我就调用谁就可以了。 

        myArr[2]();
        myArr[7]();
        myArr[5]();

 

闭包内存泄漏含义的理解

闭包形成了,让原作用域链不释放,造成了内存的浪费。而且如果是在全局作用域中形成闭包,只有在页面关闭时,本该释放的空间才会释放掉。这就占据了内存,浪费内存空间,专业术语叫——内存泄漏。按我们正常的思路理解,占内存了不应该是内存浪费吗?跟泄漏有什么关系呢?哈哈哈,这里我用成哥传授的思想解释一下哈。

想象一下,你两只手捧一摊水,时间越久手里的水是不是越来越少?流失的是不是越来越多?而且手越用力想去抓住留住水时,水流失的速度越快,这就跟内存泄漏是一样的思路。站在人的角度看,手里的水越来越少了,就相当于内存越来越少,泄漏掉了。

这就很形象了,借鉴的是成哥的例子,其实更手捧着沙子是一样,就站在人的角度看,手里的东西越来越少就像内存一样越来越少。

 

闭包的作用

实现共有变量

最典型的就是用闭包实现累加器

        function sumAdd() {
            var sum = 0;
            function Add() {
                for (var i = 1; i <= 10; i++) {
                    sum += i;
                }
                console.log(sum);
            }
            return Add;
        }
        // test引用函数b a()返回的是函数b
        var test = sumAdd();
        test(); // 执行函数

案例中的:

sum变量,本该在调用sumAdd函数调用完成之后内存就会被释放掉,但是由于sumAdd函数的返回值的Add函数,并且Add函数使用sum变量,

所以当在全局作用域中调用Add函数时,还是可以访问到sum变量的,因为sum变量存在于Add函数的作用域链中。

sum就一直存在全局作用域中,所以sum的内存没有被释放。这就导致了内存泄漏。只有当全局作用域销毁时,sum的内存才会一起被释放。

可以做缓存,跟存储结构很像

做个简单的吃食物,添加事物的案例。

        function test() {
            var food = "";
            var obj = {
                eatFood: function () {
                    if (food != "") {
                        console.log("I am eating " + food);
                        food = "";
                    } else {
                        console.log("There is nothing");
                    }
                },
                pushFood: function (myFood) {
                    food = myFood;
                }
            }
            return obj;
        }
        var person = test();
        person.eatFood();
        person.pushFood("banana");
        person.eatFood();

当然了,闭包还有其他的作用,暂时还未涉及。这是两个简单的闭包的使用,在以后的学习中,遇到了再做补充。

 

 

欧克了,今天闭包的学习结束了,大家看到了这篇文章的话,一定要好好理解理解过程,第一遍看可能懵,多看几遍,多用自己的话口述过程,渐渐的就能明白了。

 

 

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值