闭包
目录
闭包通常被视作JavaScript的高级特性,但是,理解闭包对于掌握这门语言至关重要。
闭包的定义
1.闭包:
最简单的描述,即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数及其他内部函数。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
2.闭包形成:
当其中的一个内部函数在其外部函数之外被调用时,就会形成闭包。也就是说,内部函数会在外部函数返回后被执行。而当这个内部函数执行时,它仍然必需访问其外部函数的局部变量、参数以及其他内部函数。
3.在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的应用
1.函数外部读取函数的局部变量
1 | function A (){ |
此时b就是一个闭包,由B函数和闭包创建时存在的”Benjamin”字符串形成。
2.让这些变量的值始终保持在内存中
例一:
1 |
|
例二:(形成闭包)
1 | function A (){ |
例三:
1 | function adder (x){ |
从本质上讲,adder 是一个函数工厂 — 创建将指定的值和它的参数求和的函数,在上例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5求和,另一个和 10 求和。
adder5和adder10都是闭包。它们共享相同的函数定义,但是保存了不同的环境。在 adder5 的环境中,x 为 5。而在 adder10 中,x 则为 10。
3.为函数引用设置延时
闭包的一个常见用法是在执行函数之前为要执行的函数提供参数。例如:将函数作为 setTimout 函数第一个参数传参的问题;
we all know ,如果第一个参数不需要传参,我们直接传入函数名即可。需要传参的情况下就要考虑使用闭包了。代码如下:
1 | function A (a,b){ |
4.闭包模拟私有方法(计数器)
1 | var counter = function (){ |
请注意两个计数器是如何维护它们各自的独立性的。每次调用 counter() 函数期间,其环境是不同的。每次调用中,i中含有不同的实例。
这种形式的闭包提供了许多通常由面向对象编程所享有的益处,尤其是数据隐藏和封装。
5.循环中创建闭包(常见错误)
1 | function bindFocus (){ |
运行上面代码后我们发现,无论鼠标移动到哪个input上面,其文本值显示的都是”Please input your address”。
该问题的原因在于赋给 onfocus 的函数是闭包;它们由函数定义和记录自 bindFocus 函数作用域的环境构成。一共创建了三个闭包,但是它们都共享同一个环境。在 onfocus 的回调被执行时,循环早已经完成,且此时 itemi 变量(由所有三个闭包所共享)已经指向了 item 列表中的最后一项。
如果解决此问题呢?
使用更多的闭包,代码如下:
1 | function bindFocus (){ |
所有的回调不再共享同一个环境,B函数为每一个回调创建一个新的环境。在这些环境中,itemi 指向 item 数组中对应的字符串。
闭包的作用域链
1.函数对象的[[scope]]属性、ScopeChain(作用域链)
javascript中每个函数都是一个函数对象(函数实例),既然是对象,就有相关的属性和方法。[[scope]]就是每个函数对象都具有的一个仅供javascript引擎内部使用的属性,该属性是一个集合(类似于链表结构),集合中保存了该函数在被创建时的作用域中的所有对象,而这个作用域集合形成的链表则被称为ScopeChain(作用域链)。该作用域链中保存的作用域对象,就是该函数可以访问的所有数据。
2.Execution Context(运行期上下文)、Activation Object(活动对象)
a.函数被创建时:
函数所在的全局作用域的全局对象被放置到函数的作用域链([[scope]]属性)中。此时作用域链的第一个对象保存的是全局对象,全局对象中保存了诸如this,window,document以及全局对象中的函数。这也就是我们可以在全局作用域下的函数中访问window(this),访问全局变量,访问函数自身的原因。当然还有函数作用域不是全局的情况。
b.函数开始执行时:
就会创建一个Execution Context的内部对象,该对象定义了函数运行时的作用域环境(注意这里要和函数创建时的作用域链对象[[scope]]区分,这是两个不同的作用域链对象,这样分开一是为了保护[[scope]],二是为了方便根据不同的运行时环境控制作用域链。函数每执行一次,都会创建单独的Execution Context,也就相当于每次执行函数前,都把函数的作用域链复制了一份到当前的Execution Context中)。Execution Context对象有自己的作用域链,在Execution Context创建时初始化,会将函数创建时的作用域链对象[[scope]]中的全部内容按照在[[scope]]作用域链中的顺序复制到Execution Context的作用域链中。此时,在Execution Context的作用域链的顶部会插入一个新的对象,叫做Activation Object(活动对象),这个活动对象保存了函数中的所有命名参数,局部变量,arguments(参数集合),this指针等函数内部的数据情况,这个Activation Object是一个可变对象,里面的数据随着函数执行时的数据的变化而变化,当函数执行结束之后,就会销毁Execution Context,也就会销毁Execution Context的作用域链,当然也就会销毁Activation Object(但如果存在闭包,Activation Object就会以另外一种方式存在,这也是闭包产生的真正原因)。
c.函数在运行过程中:
每遇到一个变量,都会去Execution Context的作用域链中从上到下(0->1 => 活动对象->全局对象)依次搜索,如果在第一个作用域链(假如是Activation Object,因为with及try-catch的catch子句,可以在函数运行时临时改变函数运行期上下文的作用域链,此时一个新的对象被创建,并插入到作用域链的前端)中找到了,那么就返回这个变量,如果没有找到,那么继续向下查找,直到找到为止,这也就是为什么函数可以访问全局变量,当局部变量和全局变量同名时,会使用局部变量而不使用全局变量。所以标识符所处的位置越深,读取它的速度越慢。
d.通过一个实例来分析闭包的形成过程
1 | function submitForm(id){ |
因为闭包函数B是在函数A执行时被解析的,所以我们来看看函数A执行时的,闭包的解析。
1)A执行时,B解析的情况:
当A函数中执行到闭包时,javascript引擎发现了B的存在,像A函数解析一样,将B解析,为B函数对象创建[[scope]]属性,初始化作用域链(此时B函数对象的作用域链中有两个对象(A函数执行时的Activation Object和全局对象)。
此时B对象的作用域链和A函数的执行上下文作用域链是相同的,为什么呢?因为B是在A函数执行的过程中被发现并且解析的,而A函数执行时的作用域是Activation Object,那么结果就很明显了,B被解析的时候它的作用域正是A作用域链中的第一个作用域对象Activation Object,当然,由于作用域链的关系,全局对象作用域也被引入到B的作用域链中。
2)B执行时:
当闭包B执行时,一个运行期上下文被创建,它的作用域链与[[Scope]]属性中引用的两个相同的作用域链被同时初始化,然后一个新的活动对象为闭包自身创建。此时闭包作用域链从上到下一次为:0->闭包B的活动对象,1->引用的A活动对象,2->全局对象。
此时我们发现闭包使用id和submitForm,id在1上,sumitForm在2上。这也就是闭包性能的关注点。
因此提高性能:设置缓存,将数据保存在局部变量中。
e.注意:
1)对函数的每次运行而言,每个运行期上下文都是独一的,所以多次调用同一个函数会多次创建运行期上下文,函数执行完毕,运行期上下文就会被销毁。
2)同一个父环境创建的闭包是共用一个[[scope]]属性的。也就是说,某个闭包对其中[[scope]]的变量的修改会影响到其他闭包对其变量的读取。因为闭包作用域链中的Activation Object,引用了父函数的Activation Object .
闭包的缺点
闭包有一个非常严重的问题,那就是内存浪费问题,这个内存浪费不仅仅因为它常驻内存,更重要的是,对闭包的使用不当会造成无效内存的产生。
转载申请
本作品采用知识共享署名 4.0 国际许可协议进行许可,转载时请注明原文链接,文章内图片请保留全部内容。