闭包是穷人的对象。对象是穷人的闭包。

定义

1
2
3
4
5
6
7
8
function GetCounter(){
let c = 0
function count() {return ++c}
return count
}

let counter = GetCounter()
counter()

这段代码的返回值是 undefined 还是 1 ?

答案是 1 。

因为这是 JavaScript 的一个重要特性——闭包(Closure)。

闭包是一个包含函数和其变量绑定的对象。

例如上述代码中,counter 是一个闭包。

其中函数为 function count() {return ++c} ,变量绑定为 {c: GetCounter.c}

释疑

如果你学习过 C 系列的语言,可能会感到疑惑。

函数返回了怎么还能引用其中的局部变量。

这是因为 JavaScript 有两个特性——词法作用域和一等公民函数。

先说词法作用域,简单来说词法作用域是描述查找变量的值的一种规则——从定义处查找。

1
2
3
4
5
6
7
8
9
function FunctionScope(){
function f(){
return scope
}
return f
}

let f = FunctionScope()
f()

这段代码的返回值是什么?是 ReferenceError 吗?

不一定。因为还不知道 scope 的定义。

那从哪获取 scope 的定义呢。当然是从这段代码所处的地方获取。

假设改成这样

1
2
3
4
5
6
7
8
9
10
function FunctionScope(){
function f(){
return scope
}
return f
}

let scope = "global scope"
let f = FunctionScope()
f()

那么代码的返回值就是 "global scope"。因为所处的作用域有变量绑定{scope: global.scope} (这里使用 Javascript 的对象语法来表示变量绑定)

如果改成这样

1
2
3
4
5
6
7
8
9
10
11
function FunctionScope(){
let scope = "function scope"
function f(){
return scope
}
return f
}

let scope = "global scope"
let f = FunctionScope()
f()

那么代码的返回值是"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
2
3
4
5
6
7
function counter() {
var n = 0;
return {
count: function() { return n++; },
reset: function() { n = 0; }
};
}

同一个闭包定义调用两次返回不同的闭包。

1
2
3
4
5
6
var c = counter(), d = counter(); // 创造两个 counter
c.count() // => 0
d.count() // => 0: 独立计数
c.reset() // reset() 和 count() 方法共享状态
c.count() // => 0: 因为重置了 c
d.count() // => 1: d 没有重置

闭包中的变量绑定只记录引用,不记录具体的值。

1
2
3
4
5
6
7
8
9
10
function Funcs(){
let funcs = [];
for(var i = 0; i < 5; i++){
funcs[i] = function () {return i}
}
return funcs
}

let functions = Funcs()
functions[2]()

所以这段代码返回的是 5 不是 2。因为循环结束后 i 的值为 5。

还需要注意的一点是闭包中的 this 不是外层的 this

1
2
3
4
5
6
7
8
9
function f(){
let self = this
let a = 1
function g() { return this.a }
return g
}

let h = f()
h()

这段代码会返回 undefined 因为 g 中的 this.athis 指的是 g

因为 g 中没有变量 a 的定义,所以返回 undefined

只要改成 self.a 就可以了。

此外还可以使用 ES6 的箭头函数。

1
2
3
4
5
6
7
8
9
function f(){
let self = this
let a = 1
let g = () => a
return g
}

let h = f()
h()

最后

文章的内容参考了犀牛书