# 定义
闭包 是指有权访问另一个函数作用域中的变量的函数。注意别混淆匿名函数和闭包的概念。
创建闭包 需要达到两个条件,如果不满足第二条,也只能称作是匿名函数
(1)在一个函数内部创建另一个函数
(2)内部函数访问外部函数的变量
function createCompareFn(attr) {
return function(obj1, obj2) {
return obj1[attr] >= obj2[attr]
}
}
上例中,内部函数(一个匿名函数) 访问了包含函数(即外部函数)的变量attr
,即使这个内部函数被返回且在其他地方被调用了,它仍然可以访问变量attr
。之所以能够访问,是因为内部函数的作用域链中含有包含函数的作用域。
# 作用域链
当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用arguments
和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,包含函数的活动对象处于第二位,包含函数的包含函数的活动对象处于第三位……,直到找到全局环境为止。这些活动对象使用链表来连接,形成作用域链。
在函数执行过程中,为了读取和写入变量的值,需要在作用域链中查找变量。看一个简单的函数声明及调用来解释:
function compare(val1, val2) {
return val1 >= val2
}
var result = compare(5, 10)
上述代码定义了compare
函数,然后又在全局作用域中调用。当调用compare()
时,会创建一个包含argument, val1, val2
的活动对象。而全局环境的变量对象处在作用域链的第二位
后台的每个执行环境都有一个标识变量的对象——变量对象。全局环境的变量对象始终存在,而像
compare()
这样的局部环境的变量对象,只有在函数执行过程中菜户存在。在创建compare()
函数时,会创建一个包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]
属性中,当执行compare()
函数时,回味函数创建一个执行环境,然后通过复制函数的[[Scope]]
属性中的对象构建起执行的作用域链,以此类推形成作用域链。如图所示,作用域链本身只是一个指向变量对象的指针列表,它只引用但不实际包含对象。
无论什么时候在函数中访问一个白能量时,就会从作用域链中搜索具有相应名字的变量。通常当函数执行完成后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)
# 闭包的作用域链
由于闭包存在函数内部定义函数,内部定义的函数将包含函数的活动对象到它的作用域链中。假设有闭包如下:
function createCompareFn(attr) {
return function(obj1, obj2) {
return obj1[attr] >= obj2[attr]
}
}
var compare = createCompareGn('name')
var result = compare({ name: 'Nic' }, { name: 'Goe' })
则它的作用域链关系为
由于闭包会携带它包含函数的作用域链,因此会比其他函数占用更多内存,过度使用闭包容易导致占用内存过多,需谨慎。
# 闭包与变量
由于作用域链本身只是一个指向变量对象的指针列表,它只引用并不真正存储它们。而这种配置机制引出了一个副作用,即闭包只能取到包含函数中任何变量的最终值。
因为闭包作用域链与包含函数的活动对象之间只是引用关系,当包含函数中由于某些运算导致它活动对象中的属性发生更新时,该更新会被带到闭包作用域中,当闭包再访问变量时,取到的就是被更新后的变量的值。经典例子如下:
function createFunctions() {
var result = new Array()
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i
}
}
return result // 10 个 10
}
该例中,在闭包中返回包含函数的变量i
并压入给结果数组。表象上看应该得到1~10
的数组。打印发现是10个10。原因是:每个函数的作用域链中,都保存着createFunctions()
函数的活动对象,引用的都是同一个变量i
;当createFunctions()
函数返回后,变量i
的值是10,此时每个函数都引用者保存变量i
的同一个变量对象。所以在每个函数内部i
的值都是10。作用域引用关系如下:
我们可以通过创建另一个匿名函数强制让闭包的行为符合预期
function createFunction() {
var result = new Array()
for (var i = 0; i < 10; i++) {
result[i] = function(num) {
return function() {
return num
}
}(i)
}
return result // 1~10
}
经过如上改造,我们没有闭包直接赋值给数组,取而代之的是定义了一个匿名函数,并将立即执行该函数的结果赋给数组。匿名函数有个参数num
,存在于匿名函数的作用域链中。程序执行for循环调用每个匿名函数时,由于函数参数是按值传递的,在其内部我们创建了一个直接访问num的闭包。这样一来,result数组中存储的值就是每次执行的num的一个副本了。
# 闭包的this变量
我们知道,this
对象是在运行时基于函数的执行环境绑定的。在全局函数中,this
等于window
。而当函数被某个对象的方法调用时,this
等于那个对象。
由于匿名函数的执行环境具有全局性,因此其this
对象通常指向window(除使用call()
或apply()
来改变函数执行环境外)。看以下例子:
var name = 'the window'
var object = {
name: 'my object',
getNameFn: function() {
return function() {
return this.name
}
}
}
console.log(object.getNameFn()()) // the window
本例中,getNameFn是对象中的一个方法属性。object.getNameFn()
返回一个函数,object.getNameFn()()
立即执行该函数得到一个字符串。
我们知道,每个函数在被调用时都会自动取得两个特殊变量:this
和arguments
。内部函数在搜索这两个变量时,只会搜索到它自己的活动对象为止(它们自身能获取到不必去包含函数活动对象中获取)。在本段代码执行时,程序发现需要返回逻辑想要返回的this.name
,于是搜索匿名函数自身的作用域,取到自己的this
对象,该对象因匿名函数执行环境全局性的特征指向了window
,从而输出了"the window"
。
注意本例的称呼是匿名函数而不是闭包!原因就是匿名函数内部有自身的
this
变量,它无需也无法获取到外部object
的this
,没有达到内部函数访问外部函数的这么一个行为,因此不称呼为闭包。以下改造后就符合了闭包的特征
如果我们想获取到object中的name
,只需如下简单改造即可
var name = 'the window'
var object = {
name: 'my object',
getNameFn: function() {
var that = this
return function() {
return that.name
}
}
}
console.log(object.getNameFn()()) // myobject
经过如上改造后,但执行object.getNameFn()()
调用内部闭包函数时,需要搜索that
,而在自身作用域内并没有找到that
,于是顺着作用域链查找包含函数的作用域,得到结果。
# 闭包与内存泄漏
由于闭包作用域链包含着包含函数的作用域,因此会比普通函数占用更多的内存,当使用闭包不当,且未得到合适的释放情况下,就容易造成大量内存空间的占用。看一个例子
function assignHandler() {
var element = document.getElementById('someElement')
element.onclick = function() {
alert(element.id)
}
}
以上代码实现了对某个元素进行点击时的点击响应事件。onclick
是一个闭包,在这个闭包内循环引用了element.id
。因此,只要匿名函数存在,element
的引用数至少是1
。那么,在垃圾回收机制规则中就无法判定element
是一个需要被回收的元素。导致其一直占用在内存空间中。解决办法如下
function assignHandler() {
var element = document.getElementById('someElement')
var id = element.id
element.onclick = function() {
alert(id)
}
element = null
}
如此改造有两点:(1)对element.id
保存副本目的是在闭包中取消对元素变量的循环引用。(2)由于闭包会引用包含函数的整个活动对象,其中还包含着element
,因此包含函数的活动对象中也会保存有一个引用。因此有必要把element
变量设置为null
# 闭包与应用场景
(1)模仿块级作用域
在闭包中创建的变量,不受外部变量的影响。看以下函数,在for
循环这个块级空间中定义了变量,但在外部依然能访问alert(i)
依然能访问到该变量。
function Counter(count) {
for (var i = 0; i < count; i ++) {
console.log(i)
}
var i // 重新声明变量
alert(i) // 计数
}
如何让该变量私有化?当然可以使用ES6的let
定义。本例笔者不想说这个,我们来看以下这个更熟悉的函数格式
(function() {
// 块级作用域
})()
我们都称它为自执行函数,初学者可能觉得这个格式很难理解,我们做如下拆解来帮助理解它。
一个正常函数表达式定义及调用如下
var func = function() {
// 块级作用域
}
func()
普通的函数调用使用函数名加圆括号来执行一个函数,如果改成函数对象,即如下
function() {
// 块级作用域
}() // 报错。。
执行报错了。因为JS将 function
关键字当做一个函数声明的开始,而函数声明后面是不允许跟圆括号,而函数表达式后面可以跟圆括号,因此需要把匿名函数加一个圆括号来告诉JS这是一个函数表达式,从而形成了自执行函数的表现形式,也实现了块级作用域的目的。
利用自执行函数的特征,我们可以改写outputNumber
函数如下
function outputNumber() {
(function() {
for(var i = 0; i < number; i++) {
console.log(i)
}
})()
console.log(i) // 报错
}
以上代码中,匿名函数是一个闭包,他能够访问包含函数作用域链中的所有变量,而外部无法访问比包内的变量。
(2)创建私有变量
创建一个可以访问私有变量和私有函数的共有方法。该方法也称作特权方法。
创建私有变量需要从一个计数器来说起:
function Counter() {
var count = 0
this.clear = function () {
this.count = 0
}
this.add = function() {
this.count++
}
this.decrease = function () {
this.count--
}
}
这个构造函数中,有一个私有变量count
,它只能在函数内部被访问,函数中有三个特权方法用于清空,累加和累减计数。除了它们外,灭有别的方法可以访问到count
变量。而特权方法可以被实例化的实例访问。count
就是一个私有变量
【缺点】每次实例化的时候,都需要重新生成一次特权方法
以上方法为构造函数法创建私有变量,除此之外,还有通过私有作用域定义静态私有变量,为单例创建私有变脸个特权方法的模块模式等,这里不展开说明。