详解js中的闭包
前言##
在js中,闭包是一个很重要又相当不容易完全理解的要点,网上关于讲解闭包的文章非常多,但是并不是非常容易读懂,在这里以《javascript高级程序设计》里面的理论为基础。用拆分的方式,深入讲解一下对于闭包的理解,如果有不对请指正。
写在闭包之前##
闭包的内部细节,依赖于函数被调用过程所发生的一系列事件为基础,所以有必要先弄清楚以下几个概念:
1. 执行环境和活动对象###
** - 执行环境(execution context)定义了变量或者函数有权访问的其他数据,每个执行环境都有一个与之关联的变量对象(variable object),执行环境中定义的变量和函数就保存在这个变量对象中;
全局执行环境是最外围的一个执行环境,通常被认为是window对象
执行环境和变量对象在运行函数**时生成
执行环境中的所有代码执行完以后,执行环境被销毁,保存在其中的变量和函数也随之销毁;(全局执行环境到应用退出时销毁)
2. 作用域链###
当代码在一个执行环境中执行时,会创建变量对象的一个作用域链(scope chain),作用域链用来指定执行环境有权访问的所有变量和函数的访问顺序;
作用域链的最前端,始终是当前代码执行环境的变量对象,如果这个环境是函数,则其活动对象就是变量对象
作用域链的下一个变量对象,来自外部包含环境,再下一个变量对象,来自下一个外部包含环境,以此类推直到全局执行环境
在函数执行过程,根据当前执行环境的作用域链来逐层向外查找变量,并且进行标识符解析
是不是觉得以上的理论很枯燥而且艰涩?因为基本上是从书上引用来的,不着急着理解,先摆在上面,等会结合案例回头再来看!接下来请看样例:
样例1
以这段简单的代码为例,根据上面的理论画一下关系图(直接用ps画的,原谅我拙劣的笔迹):

如图所示,在执行函数A的时候,创建了A的执行环境和变量对象,其中A的变量对象和全局变量对象中都含有a变量,根据作用域链从前向后查找,在A的变量对象中找到,所以输出1,执行完毕以后 ,A的指向环境销毁,A的变量对象由于没有被引用,所以也销毁;
样例2
这个例子比较简单,要画图的话只需要画一个全局变量对即可,因为在js中,外围环境无法访问内围局部变量(其实本质就是作用域链上找不到相应的值),所以这里会报变量未定义的错误。
样例3
上面这个例子,在函数A中定义了函数B,关系图如下:

从图上可以很清楚的看出,在每个执行环境中可以访问到的变量对象,所以B可以访问A的变量对象和全局变量对象中的变量以及自身变量对象,A可以访问自身变量对象和全局变量对象
关于执行环境和作用域链暂时说到这里,下面进入正题,讲闭包;
3. 初涉闭包###
闭包是指有权访问另一个函数作用域变量的函数,创建闭包的通常方式,是在一个函数内部创建另一个函数
上文我们提到了,由于作用域链的结构,外围函数是无法访问内部变量的,为了能够访问内部变量,我们就可以使用闭包,闭包的本质还是函数,闭包的本质还是函数****闭包的本质还是函数。
样例4
上面就是一个很简单的闭包例子,通过m函数,我们可以获得A函数内部变量的值,这个样例比较简单,看不出什么问题,接下来我们来深入了解一下。
-------------------------------从简单到复杂的分割线,请做好准备----------------------------------------------------
4. 闭包详解###
难点一:判断作用域指向的变量对象是否相同####
样例5
<script>
function A(){
var x = 1;
return function(){
x++;
console.log(x);
}
}
var m1 = A();//第一次执行A函数
m1();//2
m1();//3
var m2 = A();//第二次执行A函数
m2();//2
m1();//4
</script>
上面这个例子其实可以引出几个问题:
1.为什么连续执行m1的时候,x的值在递增?
2.定义函数m2的时候,为什么x的值重新从1开始了?
3.运行m2以后,为什么再运行m1,x还是按照之前m1的运行结果继续增长?(其实就是m1和m2里面的x为什么是相互独立,各自维持的?)
其实要解决上面的问题,我们就要用到前面铺垫的知识点了:
首先,先画一下结构图,
(额,这图画的可能真的有点丑),不要慌,图上虽然画的有点乱,但是其实很简单:左半部分和上面简单闭包的例子,其实是完全一样的,而右边半部分,与左边其实是完全对称的;注意看图上的重点:每次执行A函数时,都会生成一个A的活动变量和执行环境,执行完毕以后,A的执行环境销毁,但是活动对象由于被闭包函数引用,所以仍然保留,所以,最终剩下两个A的变量对象,因此m1和m2在操作x时,指向的是不同的数据,
现在来回答上面的三个问题:
1.(为什么连续执行m1的时候,x的值在递增?)
answer:因为m1在引用的活动对象A一直没有释放(想释放的话可以让m1=null),所以x的值一直递增。
2.定义函数m2的时候,为什么x的值重新从1开始了?
answer:因为又一次运行了A函数,生成一个新的A的活动对象,所以m2的作用域链引用的是一个新的x值。
3.m1和m2里面的x为什么是相互独立,各自维持的?
answer:因为在定义m1和m2的时候,分别运行了A函数,生成了两个活动对象,所以,m1和m2的作用域链是指向不同的A的活动对象的。
好的,到这里先回顾一下前面说到的知识点:
执行环境和变量对象在运行函数时生成
执行环境中的所有代码执行完以后,执行环境被销毁,保存在其中的变量和函数也随之销毁;(全局执行环境到应用退出时销毁)
感觉理解了吗?接下来,再看看另一个很类似的例子:
样例6
这个例子和刚刚十分类似,不同的是,在A内部就先定义了两个函数,可以看出 ,最后的结果与上面的例子有些不同:
变量x仍然能保持递增,但是m[0]和m[1]定义的函数,对于x的改变不再是相互独立的,其实大家估计猜到了,这里的m[0]和m[1]的作用域指向的A的变量对象,其实是同一个,为什么呢?很简单,看看刚刚这段代码,其实是只调用了一次A函数,再看上文那句话:
执行环境和变量对象在运行函数时生成
既然A只执行一次,那么A的活动变量当然也就生成了一个,所以这里m[0]和m[1]的作用域指向同一个A的变量对象
难点二:判断变量对象中变量的值####
样例7
<script>
function A(){
var funs=[];
for(var i=0;i<10;i++){
funs[i]=function(){
return i;
}
}
return funs;
}
var funs = A();//定义funs[0]-funs[9],10个函数
console.log(funs[0]());//10
console.log(funs[1]());//10
console.log(funs[6]());//10
</script>
这个例子其实算是一个经典案例,在很多地方都有提到,执行完毕后 funs数组中,funs[0]-funs[9]存的其实都是一样的,都是一个返回i值的函数,这个例子容易错误的地方其实在于,弄错了产生执行环境的时机,还是看这句话:
执行环境和变量对象在运行函数时生成
所以,当执行 var funs = A();时,只是定义函数,而没有执行,真正产生环境变量的时间是在console.log(funs[0]());这三句的时候,此时A的变量对象中i值是什么呢?很简单,看它return的时候,i的值,显然,i的值是10,所以,最后三句输出的都是10
好的,针对以上的案例,如果我就是想让fun[i]能够返回i,那应该怎么写呢?在《javascript高级程序设计》中,提供了一种参考的写法:
样例8
是不是一看头就大了?没关系,接下来我们慢慢分析,当然,上述代码中anonymous1和anonymous2两个名字是我自己添加上的,为了后面能够更好的说明。
首先,先来看看function anonymous1(num){}(i),这是一个立即执行函数,效果和名字一样,定义完之后马上运行结果,那这里运行的结果是什么呢?就是把i的值立即传递给num这个局部变量,然后再返回anonymous2,请注意这个立即执行函数被执行的次数,10次,再来看看这句话
执行环境和变量对象在运行函数时生成
好的,那现在请回答我:
这里面生成了几个anonymous1的活动变量?
answer:当然也是10个,
那每个anonymous1活动变量中存贮的num值是多少?
answer:看anonymous函数return的时候可以知道,存贮的num值就是每次传入的i值,也就是0-9
好了,那现在很明了了,这样的写法其实相当于,把每次的i值都保存在一个anonymous1活动变量钟,给最内层的anonymous2函数使用
小结##
写到这里,关于闭包的主要特征和辨别方式已经基本讲到了,个人感觉因为这个问题比较抽象,还是多看看文中以及网上的一些例子,加深理解。以上内容属于个人见解,如果有不同意见,欢迎指出和探讨。希望能对看到的人有所帮助,同时,码字不易(尤其是还要配上灵魂画师级别的配图~),请尊重作者的版权,转载请注明出处,如作商用,请与作者联系,感谢!
详解js中的闭包的更多相关文章
- 详解js中的寄生组合式继承
寄生组合式继承是js中最理想的继承方式, 最大限度的节省了内存空间. js中的寄生组合式继承要求是: 1.子对象有父对象属性的副本, 且这些不应该保存在子对象的prototype上. 2. ...
- 详解JS中DOM 元素的 attribute 和 property 属性
一.'表亲戚':attribute和property 为什么称attribute和property为'表亲戚'呢?因为他们既有共同处,也有不同点. attribute 是 dom 元素在文档中作为 h ...
- 详解Js中文件读取机制
前言,文件读取是提高应用体验度的必须接口,应用场景中需求很频繁. Js处理文件读取,由于处于安全方面的考虑,在2000年以前,都是以“<input type="file"&g ...
- 详解javascript中的闭包
全局变量与局部变量 在说闭包之前先说明全局变量与局部变量 全局变量:变量声明时如果不使用 var 关键字,那么它就是一个全局变量,即便它在函数内定义. 局部变量:使用var关键字定义 全局变量/局部变 ...
- 详解js中的apply与call的用法
前言 call 和 apply 都是为了改变某个函数运行时的 context 即上下文而存在的,换句话说,就是为了改变函数体内部 this 的指向.call 和 apply二者的作用完全一样,只是接受 ...
- 详解JS中Number()、parseInt()和parseFloat()的区别
三者的作用: Number(): 可以用于任何数据类型转换成数值: parseInt().parseFloat(): 专门用于把字符串转换成数值: 一.Number( ): (1)如果是Boolean ...
- 详解 JS 中 new 调用函数原理
JavaScript 中经常使用构造函数创建对象(通过 new 操作符调用一个函数),那在使用 new 调用一个函数的时候到底发生了什么?先看几个例子,再解释背后发生了什么. 1)看三个例子 1.1 ...
- 详解js中的this指向
this指向问题是个老生常谈的问题了,现在我给大家一个例子 var obj={ bar:'Cynthia' , foo:function(){ console.log(this.bar,"w ...
- 详解JS中 call 方法的实现
摘要:本文将全面的,详细解析call方法的实现原理 本文分享自华为云社区<关于 JavaScript 中 call 方法的实现,附带详细解析!>,作者:CoderBin. 本文将全面的,详 ...
随机推荐
- 数据结构--栈 codevs 1107 等价表达式
codevs 1107 等价表达式 2005年NOIP全国联赛提高组 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 钻石 Diamond 题目描述 Descripti ...
- ACdream OJ 1099 瑶瑶的第K大 --分治+IO优化
这题其实就是一个求数组中第K大数的问题,用快速排序的思想可以解决.结果一路超时..原来要加输入输出优化,具体优化见代码. 顺便把求数组中第K大数和求数组中第K小数的求法给出来. 代码: /* * th ...
- C++基础笔记(二)C++对C的扩展
Xcode创建C++项目 1.新建一个MAC工程(command line tool) 2.导入头文件 3.修改文件后缀(*.m-->*.mm) 4.修改主函数中的OC代码为C++的代码 动 ...
- Mecanim的Avater
角色共用同一套动作原理 先说说为什么不同的角色可以共用同一套动作:因为导入之后,我们需要为它们每一个模型都创建一个Avater,而Avater里存储了骨骼的蒙皮信息(创建Avater时把三维软件里的蒙 ...
- 3D跑酷遇到的问题
前言 项目名称:3D跑酷项目 写作目地:使用Asset Server进行多人协作开发过程中,记录遇到的问题 问题1:UIAtlas无法自动更新 描述:NGUI的Atlas图集(图片)无法自动更新 后果 ...
- 基于PXC的MySQL高可用环境简单部署
PXC简介 Percona XtraDB Cluster(简称PXC集群)提供了MySQL高可用的一种实现方法. 1.集群是有节点组成的,推荐配置至少3个节点,但是也可以运行在2个节点上. 2.每个节 ...
- JavaScript Number 对象 Javascript Array对象 Location 对象方法 String对象方法
JavaScript Number 对象 Number 对象属性 属性 描述 constructor 返回对创建此对象的 Number 函数的引用. MAX_VALUE 可表示的最大的数. MIN_V ...
- javascript中的栈结构
1.栈的定义 栈是一种和列表类似的数据结构,可以用它来解决很多的编程问题,栈是一种高效的数据结构,因为数据只能在栈的顶端添加或者删除,所以这样的操作很快而且容易实现. 栈是一种特殊的列表,站内的元素只 ...
- UWP 解压 GZIP
准备工作: 通过 NUGET 安装 Microsoft.Bcl.Compression ; 使用命名空间 using System.IO.Compression ; public static asy ...
- VisualStudio2013+EF6+MySql5.5环境下配置
看院子里对EF框架和MySql的配置文章不少,但是几乎出自一篇文章的转载,而且这篇转载的文章的也比较坑爹,下面我将介绍一下我的配置过程: 第一步:安装mysql-connector-net-6.9.9 ...