好了,夸张和对电影的无耻引用够多了。
为了理解和识别闭包,这里有一个你需要知道的简单粗暴的定义:
闭包就是函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域之外执行时。
让我们跳进代码来说明这个定义:
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
根据我们对嵌套作用域的讨论,这段代码应当看起来很熟悉。由于词法作用域查询规则(在这个例子中,是一个 RHS 引用查询),函数 bar()
可以 访问 外围作用域的变量 a
。
这是“闭包”吗?
好吧,从技术上讲…… 也许是。但是根据我们上面的“你需要知道”的定义…… 不确切。我认为解释 bar()
引用 a
的最准确的方式是根据词法作用域查询规则,但是那些规则 仅仅 是闭包的(一个很重要的!)一部分。
从纯粹的学院派角度讲,上面的代码段被认为是函数 bar()
在函数 foo()
的作用域上有一个 闭包(而且实际上,它甚至对其他的作用域也可以访问,比如这个例子中的全局作用域)。换一种略有不同的说法是,bar()
闭住了 foo()
的作用域。为什么?因为 bar()
嵌套地出现在 foo()
内部。就这么简单。
但是,这样一来闭包的定义就是不能直接 观察到 的了,我们也不能看到闭包在这个代码段中 被行使。我们清楚地看到词法作用域,但是闭包仍然像代码后面谜一般的模糊阴影。
让我们考虑这段将闭包完全带到聚光灯下的代码:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 -- 哇噢,看到闭包了,伙计。
函数 bar()
对于 foo()
内的作用域拥有词法作用域访问权。但是之后,我们拿起 bar()
,这个函数本身,将它像 值 一样传递。在这个例子中,我们 return
bar
引用的函数对象本身。
在执行 foo()
之后,我们将它返回的值(我们的内部 bar()
函数)赋予一个称为 baz
的变量,然后我们实际地调用 baz()
,这将理所当然地调用我们内部的函数 bar()
,只不过是通过一个不同的标识符引用。
bar()
被执行了,必然的。但是在这个例子中,它是在它被声明的词法作用域 外部 被执行的。
foo()
被执行之后,一般说来我们会期望 foo()
的整个内部作用域都将消失,因为我们知道 引擎 启用了 垃圾回收器 在内存不再被使用时来回收它们。因为很显然 foo()
的内容不再被使用了,所以看起来它们很自然地应该被认为是 消失了。
但是闭包的“魔法”不会让这发生。内部的作用域实际上 依然 “在使用”,因此将不会消失。谁在使用它?函数 bar()
本身。
有赖于它被声明的位置,bar()
拥有一个词法作用域闭包覆盖着 foo()
的内部作用域,闭包为了能使 bar()
在以后任意的时刻可以引用这个作用域而保持它的存在。
bar()
依然拥有对那个作用域的引用,而这个引用称为闭包。
所以,在几微秒之后,当变量 baz
被调用时(调用我们最开始标记为 bar
的内部函数),它理所应当地对编写时的词法作用域拥有 访问 权,所以它可以如我们所愿地访问变量 a
。
这个函数在它被编写时的词法作用域之外被调用。闭包 使这个函数可以继续访问它在编写时被定义的词法作用域。
当然,函数可以被作为值传递,而且实际上在其他位置被调用的所有各种方式,都是观察/行使闭包的例子。
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 看妈妈,我看到闭包了!
}
我们将内部函数 baz
传递给 bar
,并调用这个内部函数(现在被标记为 fn
),当我们这么做时,它覆盖在 foo()
内部作用域的闭包就可以通过 a
的访问观察到。
这样的函数传递也可以是间接的。
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将`baz`赋值给一个全局变量
}
function bar() {
fn(); // 看妈妈,我看到闭包了!
}
foo();
bar(); // 2
无论我们使用什么方法将内部函数 传送 到它的词法作用域之外,它都将维护一个指向它最开始被声明时的作用域的引用,而且无论我们什么时候执行它,这个闭包就会被行使。