ES 基础 —— 闭包
闭包(Closure)是函数式编程中的概念,出现于 20 世纪 60 年代。最早实现闭包的语言是 Scheme,之后闭包这个特性被其他语言吸纳。
闭包的严格定义:由函数(环境)及其封闭的自由变量组成的集合体。 ES 中的每个函数都是一个闭包,但通常只有嵌套的函数才能够体现出闭包的特性。 比如:
const genCounter = function () {
let count = 0;
return function () {
count = count + 1
return count
}
}
const counter = genCounter()
console.log(counter()) // 1
console.log(counter()) // 2
console.log(counter()) // 3
上面的代码中, genConuter()
函数中有一个初始值为 count
的局部变量,并返回了一个函数 —— 给 count
加 1,并返回 count
。之后调用 genCounter()
获得了 counter
函数,反复调用 counter
函数,获得了 1, 2, 3。
看起来蛮简单,其实有些玄机。
通常来讲,人们会这么理解:count
是 genCounter()
函数内部的变量,它的声明周期就是 genCounter()
被调用的时期,当 genCounter()
调用结束后, count
变量就被销毁了,也就无法被访问到了。
可是现在: genCounter()
调用结束后, counter()
却引用了「已经被销毁」的 count
变量,而且没有出错。
这是怎么回事?这就是闭包的特性。
闭包的实现原理
一般来说,函数调用完毕后,其 VO 就会被销毁。但在之前的代码中,即使 genCounter()
函数调用完毕后,count
还可以被内部函数访问。之所以能够访问,是因为内部函数的作用域链中包含 genCounter()
函数的 VO。下面说说其中的细节。
在介绍 Execution Context 时,详细描述了函数执行的细节,可以回顾一下。
在一个函数(外部函数)内部定义另一个函数(内部函数)时,内部函数会把外部函数的 VO 添加到自己的作用域链中。因此,在内部函数从 genCounter()
中被返回后,它的作用域链包含:
- 自身的 VO
-
genCounter()
的 VO - 全局 VO
这样,内部函数就可以访问在 genCounter()
中定义的变量。当 genCounter()
函数返回后,其作用域链会被销毁,但是由于内部函数的作用域链仍然在引用这个 VO,所以其 VO 会被保存 。直到内部函数的作用域链被销毁后, genCounter()
的 VO 才会被销毁。
由于闭包的作用域链会始终携带外部函数的作用域,因此会比其他函数占更多的内存。建议只在绝对必要时再考虑使用闭包。
闭包的应用
封装(实现私有属性)
ES 中的对象并没有私有属性,对象的每一个属性都是暴露给外部的。 虽然通过约定 —— 在私有属性前加下划线(如 _secret
)可以表示「这个属性是私有的,外部对象不应该直接读写它」。但这也只是个约定而已。如果就是有人要来直接修改这个属性,有没有更严格的机制呢? 通过闭包来实现 —— 使得属性不能被外部随意修改,同时又可以通过指定的函数接口来操作。 比如:
const foo = function() {
let secret = 'secret'
// 闭包内的函数可以访问 secret,而 secret 对于外部却是隐藏的
return {
get_secret: function () {
// 通过定义的函数接口来访问 secret
return secret
},
new_secret: function (new_secret) {
// 通过定义的函数接口来修改 secret
secret = new_secret
}
}
}()
foo.get_secret(); // 'secret'
foo.secret; // Type error
foo.new_secret('a new secret'); // 通过函数接口,修改 secret 变量
foo.get_secret(); // 'a new secret'
其他参考:
闭包的反模式
在多层嵌套函数中使用闭包
记住,多层嵌套函数中,每当要使用一个变量,总要通过 Scope Chain 寻找这个变量,变量所处的嵌套层数越深,在查找这个变量上所花费的时间就越多。
闭包中的变量冲突
function creationFunctions() {
var result = new Array()
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i
}
}
return result
}
这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返回自己的索引值。但实际上,每个函数都返回 10。因为每个函数的作用域链中都引用着 createFunctions()
函数的 VO,所以它们引用的都是同一个变量 i
。当 createFunctions()
函数返回后,变量 i
的值是 10。之后再调用函数数组中的每个函数时都会返回 10。
闭包中的 this
在闭包中使用
this
需要特别注意。
const name = 'Global'
const object = {
name: 'My Object',
getNameFunc: function () {
return function () {
return this.name
}
}
}
// object.getNameFunc() 返回的匿名闭包函数不是作为对象的方法被调用的,
// 所以其中的 this 指向全局对象
console.log(object.getNameFunc()()); // Global (非严格模式)