写点什么

JS 完美收官之——闭包

用户头像
法医
关注
发布于: 2 小时前

在上一篇 JS 完美收官之作用域中,我们已经知道当函数执行完毕后,它所产生的执行期上下文会被销毁,用完就丢掉,而今天我们探究的是闭包却与之相反,好了,我们一起来看看到底什么是闭包?


直接上代码:


function a(){       function b(){                var bbb = 234;                console.log(aaa);            }             var aaa = 123;             return b;    }    var glob = 100;    var demo = a();demo();
复制代码


当 a 函数被定义的时候会生成全局的执行期上下文 GO(global object)放在作用域链的第 0 位,紧接着在 a 函数执行的前一刻会生成局部的执行期上下文 AO(activation object)放在作用域链最顶端(第 0 位是最顶端,1 是次顶端,查找顺序是从最顶端往下查)。看图:




然而在上面代码中 a 函数的执行产生 b 函数的定义,并且 b 函数被 return 保存了出来,b 函数的定义是站在 a 函数的劳动成果之上,所以看图:



当 a 函数执行完毕之后会销毁自己的执行期上下文,咣当一下把自身作用域链上那条线剪断了,a 销毁后回到被定义的状态等待下次被执行,但并不代表 a 函数里面的执行期上下文跟着消失了,因为 b 还存着 a 函数执行期上下文的引用,紧紧拽着它呢。b 被保存全局 demo 中,当执行 demo()的时候,会生成一个独一无二的执行期上下文放到作用域链的最顶端,随后执行 console.log(aaa),由于自身上没有 aaa,那么它会顺着作用域链自上而下一次查找,此时作用域链上绑着三条引用,如图:



以上过程其实就是闭包,但凡内部函数被保存到了外部,它一定生成闭包。闭包是 JavaScript 最强大的特性之一,因为它允许函数可以访问除局部作用域之外的数据。


闭包的弊端:当内部函数被保存到外部的时候一定生成闭包,闭包会导致原有的作用域链不释放造成内存泄漏。


那啥叫作用域链不释放啊,本来 a 函数执行完毕后要销毁自身的执行期上下文从而羽化登仙,但是由于 b 函数死活拽着 a 函数的脚丫子不让走,时间久了,天干物燥,b 函数身体水分蒸发过快,造成缺水。(水分从毛孔蒸发的过程就是内存泄漏,内存用的越多剩的越少,就像泄漏了一样),要是 b 函数牛劲犯了,拽着好几百个死活不放手,就会导致系统空间过多被占用,会影响执行速度,在脚本编程中,一定要非常小心地使用闭包,因为它同时关系到内存和执行速度,我们通常把跨作用域变量存储在局部变量中,然后直接访问局部变量。以此来减轻闭包对执行速度的影响。


闭包的小例子:


  1. 实现公有变量(写个不依赖外部变量的累加器)


function add(){    var count = 0;    function demo(){        count ++;        console.log(count);    }    return demo; }var counter = add();counter();       counter();
复制代码


执行结果:



我们可以用闭包来做一个不依赖外部变量的累加器,调用多少次就会加多少次,这样的好处可以把局部变量变成私有状态,减少了全局变量的使用,全局变量处在作用域链的最底层,位置越深执行速度就会越缓慢,具体慢多少还得取决于浏览器,所以我们在写程序的时候尽量使用局部变量


⚠️全局变量还有一个非常重要的问题:那就是会发生命名冲突,比如说第三方库里面定义了一个全局变量 global,然后又在函数里面定义了一个全局变量 global,这样就会出现问题,后面的 global 覆盖前面那个 global,那第三方库可能就失效了。在下一篇文章中,我们可以一起探讨下最小化全局变量的方法,比如说立即执行函数、命名空间的模式、用 var 声明变量.......


  1. 写一个打印 0~9 数字


依旧先看代码:


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


打印结果:



看到这结果是不是跟我们想的不一样!那为什么会打印 10 个 10 呢?第一点我们要注意的是执行语句并不是一定义就执行的,console.log(i)里面的 i 的值不是立即打印的,而是要等被保存到外部函数的执行才打印,这段代码创建了 10 个闭包,并将它们存储在一个数组中,数组中的 10 个函数分别与 test 函数形成闭包并且共享 test 函数生成的执行期上下文,也就是共享变量 i ,当 test()返回时,变量 i 的值为 10,所以闭包都共享这个值,因此数组中的函数的返回值都是同一个值。


那有什么办法可以解决这个问题吗?我们就是想让它打印 0~9,该如何处理呢?


我们可以用立即执行函数解决,看如下代码:


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 (var j = 0; j < 10; j++) {        myArr[j]();     }
复制代码


加了立即执行函数之后,可以实现的的效果就是让 i 执行到第 6 行的时候立即打印出来 ,把 i 当做实参传给形参 j ,当下面代码:


   arr[j] = function () {            console.log(j);       }
复制代码


被保存到外部时,拿到的是立即执行函数的所产生的执行期上下文,与立即执行函数形成闭包,由于在 for 循环中,会产生 10 个独一无二的立即执行函数,立即执行函数里面的函数分别保存了各自的立即执行函数的执行期上下文,所以当里面的函数被保存到外部执行的时候就会打印各自保存的值。



好了以上就是今天的发分享,各位玩安!

发布于: 2 小时前阅读数: 3
用户头像

法医

关注

公众号@前端猎手 2020.07.17 加入

我是法医,一只治疗系前端码猿🐒,与代码对话,倾听它们心底的呼声,期待着大家的点赞👍与关注➕。 [微信:wKavin]

评论

发布
暂无评论
JS完美收官之——闭包