《你不知道的JavaScript》笔记-闭包

闭包就好像从 JavaScript 中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够到达那里。但实际上它只是一个普通且明显的事实,那就是我们在词法作用域的环境下写代码,而其中的函数也是值,可以随意传来传去。

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

闭包的实质

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

1
2
3
4
5
6
7
8
9
10
11
function foo() {
var a = 2;

function bar() {
console.log(a); // 2
}

bar();
}

foo();

这段代码看起来和嵌套作用域中的示例代码很相似。基于词法作用域的查找规则,函数 bar()可以访问外部作用域中的变量 a(这个例子中的是一个 RHS 引用查询)。
这是闭包吗?
技术上来讲,也许是。但根据前面的定义,确切地说并不是。我认为最准确地用来解释 bar()对 a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却是非常重要的一部分!)

下面我们来看一段代码,清晰地展示了闭包:

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
var a = 2;

function bar() {
console.log(a);
}

return bar;
}

var baz = foo();

baz(); // 2 ———— 朋友,这就是闭包的效果。

函数 bar()的词法作用域能够访问 foo()的内部作用域。然后我们将 bar()函数本身当作一个值类型进行传递。
在 foo()执行后,其返回值(也就是内部的 bar()函数)赋值给变量 baz 并调用 baz(),实际上只是通过不同的标识符引用调用了内部的函数 bar()。

bar()显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。

在 foo()执行后,通常会期待 foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo()的内容不会再被使用,所以很自然地会考虑对其进行回收

而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是 bar()本身在使用。

拜 bar()所声明的位置所赐,它拥有涵盖 foo()内部作用域的闭包,使得该作用域能够一直存活,以供 bar()在之后任何时间进行引用。

bar()依然持有对该作用域的引用,而这个引用就叫作闭包

当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
var a = 2;

function baz() {
console.log(a); // 2
}

bar(baz);
}

function bar(fn) {
fn(); // 妈妈快看呀,这就是闭包!
}

传递函数当然也可以是间接的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var fn;

function foo() {
var a = 2;

function baz() {
console.log(a);
}

fn = baz; // 将baz分配给全局变量
}

function bar() {
fn(); // 妈妈快看呀,这就是闭包!
}

foo();

bar(); // 2