JavaScript 闭包
闭包是穷人的对象。对象是穷人的闭包。
定义
1 | function GetCounter(){ |
这段代码的返回值是 undefined
还是 1 ?
答案是 1 。
因为这是 JavaScript 的一个重要特性——闭包(Closure)。
闭包是一个包含函数和其变量绑定的对象。
例如上述代码中,counter
是一个闭包。
其中函数为 function count() {return ++c}
,变量绑定为 {c: GetCounter.c}
。
释疑
如果你学习过 C 系列的语言,可能会感到疑惑。
函数返回了怎么还能引用其中的局部变量。
这是因为 JavaScript 有两个特性——词法作用域和一等公民函数。
先说词法作用域,简单来说词法作用域是描述查找变量的值的一种规则——从定义处查找。
1 | function FunctionScope(){ |
这段代码的返回值是什么?是 ReferenceError
吗?
不一定。因为还不知道 scope
的定义。
那从哪获取 scope
的定义呢。当然是从这段代码所处的地方获取。
假设改成这样
1 | function FunctionScope(){ |
那么代码的返回值就是 "global scope"
。因为所处的作用域有变量绑定{scope: global.scope}
(这里使用 Javascript 的对象语法来表示变量绑定)
如果改成这样
1 | function FunctionScope(){ |
那么代码的返回值是"function scope"
。为什么不是 "global scope"
呢?
因为定义函数同时会定义作用域。
定义 FunctionScope
的时候也定义了 {scope: FunctionScope.scope}
将这个作用域插入到作用域链中,整个作用域链为
1 | {scope: FunctionScope.scope} -> {scope: global.scope} |
按照词法作用域的规则,变量的值从定义处查找。所以先从 {scope: FunctionScope}
中查找。
发现有记录,通过引用 FunctionScope.scope
找到值 "function scope"
。所以返回 "function scope"
。如果找不到,就顺着作用域链查找直到找不到抛出 ReferenceError
如果仅仅有词法作用域是不够的。C 语言也是词法作用域,但是并没有听说过闭包。
这是因为 Javascript 将函数视为一等公民。
所以函数可以随意的创建,赋值到变量,传递为参数,作为返回值等。
这里最重要的是可以在函数中定义函数。
JavaScript 定义函数的时候会创建函数作用域,将当前环境中的变量通过引用的方式记录下来。
在上述例子中,f()
定义的同时定义了函数作用域 {scope: FunctionScope.scope}
返回 f()
也会连同函数作用域一同返回。
如果一个变量没有引用,就会被垃圾收集器回收。比如函数中的局部变量,在函数执行完后没有引用就会被回收。
但是如果返回了函数,比如说 f()
因为函数作用域 {scope: FunctionScope.scope}
引用了 scope
所以 scope
不会被回收。
所以函数执行完后还可以使用局部变量。
注意
理论上所有函数都是闭包,但是因为有的函数没有访问内部变量,所以没有闭包的感觉。
返回函数是定义闭包的一种方式,但不是唯一方式。
比如还可以返回一个对象。
1 | function counter() { |
同一个闭包定义调用两次返回不同的闭包。
1 | var c = counter(), d = counter(); // 创造两个 counter |
闭包中的变量绑定只记录引用,不记录具体的值。
1 | function Funcs(){ |
所以这段代码返回的是 5 不是 2。因为循环结束后 i
的值为 5。
还需要注意的一点是闭包中的 this
不是外层的 this
1 | function f(){ |
这段代码会返回 undefined
因为 g
中的 this.a
的 this
指的是 g
。
因为 g
中没有变量 a
的定义,所以返回 undefined
。
只要改成 self.a
就可以了。
此外还可以使用 ES6 的箭头函数。
1 | function f(){ |
最后
文章的内容参考了犀牛书。