什么是闭包

这里是修真院前端小课堂,每篇分享文从

【背景介绍】【知识剖析】【常见问题】【解决方案】【编码实战】【扩展思考】【更多讨论】【参考文献】

八个方面深度解析前端知识/技能,本篇分享的是:

【 什么是闭包】

今天给大家介绍一下什么是闭包。

1.背景介绍

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

JS函数的执行依赖于变量作用域, 函数对象的内部状态包含函数自身的逻辑,还必须引用当前的作用域链。 函数对象可以相互关联起来,函数体内部的变量可以保存在函数作用域内, 具有这种特性的函数称为闭包。

2.知识剖析

作用域和作用域链

在说到闭包的同时,我们不得不说一下作用域和作用域链,js的执行过程分为两个阶段,第一阶段是代码编译阶段,第二阶段是代码执行阶段。在编译阶段,我们的代码将会被翻译成可执行的代码、并且在此阶段我们的作用域规则将会被确定。在第二阶段,主要任务是执行可执行代码,重点注意下,我们的执行上下文将会在此时生成

执行上下文

执行上下文分为两个阶段:创建阶段:在进入函数的时候创建执行上下文,这个创建的操作中,包括了生成变量对象,建立作用域链、确定this指向。

执行阶段:包括变量赋值、函数引用、执行其他代码。经历过这两个阶段之后,它就会等待被js的垃圾回收机制回收

它在创建执行上下文的时候,先生成了变量对象用于保存我们的变量,在这个变量对象中,以function名称为属性名的变量将会储存一个指向function的指针,如果后面存在着同名的函数,那么前面的将会被覆盖,而以var形式赋值的变量将会以这个变量名为属性名储存但是值是undefined,并且,如果具有同名的变量名,会直接跳过,不会被覆盖

它在执行阶段,这个时候才会为var的进行赋值操作。并且执行引用的函数。

作用域链

作用域链是由当前环境和上层环境的一系变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问

在上面的例子中,全局,函数test,函数innerTest的执行上下文先后创建。我们设定他们的变量对象分别为VO(global),VO(test), VO(innerTest)。而innerTest的作用域链,则同时包含了这三个变量对象,所以innerTest的执行上下文可如下表示。

是的,你没有看错,我们可以直接用一个数组来表示作用域链,数组的第一项scopeChain[0]为作用域链的最前端,而数组的最后一项,为作用域链的最末端,所有的最末端都为全局变量对象。

很多人会误解为当前作用域与上层作用域为包含关系,但其实并不是。以最前端为起点,最末端为终点的单方向通道我认为是更加贴切的形容。如图。

所以作用域链是由一系列变量对象组成的,我们可以在这个单向通道中查询变量对象的标识符,这样就能够访问到上一层作用域中的变量了

垃圾回收机制

在js中存在着特殊的内存回收机制,就是当一个值在内存中失去引用时,垃圾回收机制会根据它的方法找到它,将它回收,释放内存,我们上面说过,在该函数的执行上下文声明周期结束以后,那么该段函数的执行上下文就失去了引用,其所占用的内存空间就会很快的被垃圾回收机制收回,闭包的存在会阻止这一过程

所以现在,我们可以总结一下闭包的作用

闭包可以用在许多地方。它的最大用处有两个:

1.可以访问该函数作用域以外的变量对象

2.让这些变量的值始终保存在内存中

读取函数内部的变量的例子:

/*使用闭包读取函数内部的变量*/

function f1(){

n = 999;

function f2(){

alert(n);

}

return f2;

}

var result = f1();

result(); //999

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗

变量的值始终保存在内存的例子:

/*使用闭包让函数内部的变量储存在内存中*/

function f1(){

var n = 999;

nAdd = function(){

n+=1;

};

function f2(){

alert(n);

}

return f2;

}

var result = f1();

result();//999

nAdd();

result();//1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。因为f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

3.常见问题

window.onload = function(){

var el = document.getElementById("id");

el.onclick = function(){

alert(el.id);

}

}

这段代码为什么会造成内存泄露?

4.解决方案

内存泄漏的原因:执行这段代码的时候,将匿名函数对象赋值给el的onclick属性;然后匿名函数内部又引用了el对象,存在循环引用,所以不能被垃圾回收机制回收;

修改后:

window.onload = function(){

var el = document.getElementById("id");

var id = el.id; //解除循环引用

el.onclick = function(){

alert(id);

}

el = null; // 将闭包引用的外部函数中活动对象清除

}

5.编码实战

点击按钮会弹出相应的数字0、1、2、3、4。

function init({

var pAry = document.getElementsByTagName("button");

for( var i=0; i< pAry.length; i++ ) {

(function(arg){

pAry[i].onclick = function() {

alert(arg);

};

})(i);//调用时参数

}

}

思路:加一层闭包,i以局部变量形式传递给内存函数,在js任务4中的杀人游戏选中的身份死亡有用到。

6.扩展思考

闭包还可以有什么作用?

封装变量

7.参考文献

参考一:阮一峰的网络日志:学习Javascript闭包

参考二:慕课网

参考三:详细图解作用域链与闭包

今天的分享就到这里啦,欢迎大家点赞、转发、留言、拍砖~

8.PPT视频链接

PPT:https://ptteng.github.io/PPT/PPT/closebag.html#/

视频: https://pan.baidu.com/s/1jHI4JFw 

------------------------------------------------------------------------------------------------------------------------

技能树.IT修真院

“我们相信人人都可以成为一个工程师,现在开始,找个师兄,带你入门,掌控自己学习的节奏,学习的路上不再迷茫”。

这里是技能树.IT修真院,成千上万的师兄在这里找到了自己的学习路线,学习透明化,成长可见化,师兄1对1免费指导。快来与我一起学习吧 !

链接:http://www.jnshu.com/login/1/15996364 

更多内容,可以加入IT交流群565734203与大家一起讨论交流

这里是技能树·IT修真院:https://www.jnshu.com,初学者转行到互联网的聚集地