第5章:作用域闭包
1 什么是闭包
为了理解和识别闭包,这里有一个你需要知道的简单粗暴的定义:
闭包就是函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域之外执行时。
从代码中理解:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 -- 哇噢,看到闭包了,伙计。
复制代码
foo()
被执行之后,一般说来我们会期望 foo()
的整个内部作用域都将消失,因为我们知道引擎 启用了 垃圾回收器 在内存不再被使用时来回收它们。因为很显然 foo()
的内容不再被使用了,所以看起来它们很自然地应该被认为是 消失了。
但是闭包的“魔法”不会让这发生。内部的作用域实际上 依然 “在使用”,因此将不会消失。谁在使用它?函数 bar()
本身。
有赖于它被声明的位置,bar()
拥有一个词法作用域闭包覆盖着 foo()
的内部作用域,闭包为了能使 bar()
在以后任意的时刻可以引用这个作用域而保持它的存在。
bar()
依然拥有对那个作用域的引用,而这个引用称为闭包。这个函数在它被编写时的词法作用域之外被调用。闭包 使这个函数可以继续访问它在编写时被定义的词法作用域。
函数可以被作为值传递,而且实际上在其他位置被调用的所有各种方式,都是观察/行使闭包的例子。
function foo() {
var a = 2;
function baz() {
console.log(a); // 2
}
bar(baz);
}
function bar(fn) {
fn();
}
复制代码
实质上无论何时何地只要你将函数作为头等的值看待并将它们传来传去的话,你就可能看到这些函数行使闭包。计时器、事件处理器、Ajax请求、跨窗口消息、web worker、或者任何其他的异步(或同步!)任务,当你传入一个 回调函数,你就在它周围悬挂了一些闭包!
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait("Hello, closure!");
复制代码
内部函数(名为 timer)将它传递给 setTimeout(..)。但是 timer 拥有覆盖 wait(..) 的作用域的闭包,实际上保持并使用着对变量 message 的引用。
或者,如果你信仰jQuery(或者就此而言,其他的任何JS框架):
function setupBot(name, selector) {
$(selector).click(function activator() {
console.log("Activating: " + name);
});
}
setupBot("Closure Bot 1", "#bot_1");
setupBot("Closure Bot 2", "#bot_2");
复制代码
2 循环 中的简单应用
解决循环中作用域的问题:
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
// 得到“6”被打印5次,一秒一个
复制代码
首先,让我们解释一下“6”是从哪儿来的。循环的终结条件是 i
不 <=5
。第一次满足这个条件时 i
是6。所以,输出的结果反映的是 i
在循环终结后的最终值。
超时的回调函数都将在循环的完成之后立即运行。实际上,就计时器而言,即便在每次迭代中它是 setTimeout(.., 0)
,所有这些回调函数也都仍然是严格地在循环之后运行的,因此每次都打印 6
。
修改:
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
复制代码
加入let
声明更好的解决方案:
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
复制代码
3 模块
还有其他的代码模式利用了闭包的力量,但是它们都不像回调那样浮于表面。让我们来检视它们中最强大的一种:模块。
行使模块模式有两个“必要条件”:
- 必须有一个外部的外围函数,而且它必须至少被调用一次(每次创建一个新的模块实例)。
- 外围的函数必须至少返回一个内部函数,这样这个内部函数才拥有私有作用域的闭包,并且可以访问和/或修改这个私有状态
3.1 模块模式
考虑这段代码:
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
复制代码
让我们检视关于这段代码的一些事情。
首先,CoolModule()
只是一个函数,但它 必须被调用 才能成为一个被创建的模块实例。没有外部函数的执行,内部作用域的创建和闭包都不会发生。
第二,CoolModule()
函数返回一个对象,通过对象字面量语法 { key: value, ... }
标记。这个我们返回的对象拥有指向我们内部函数的引用,但是 没有 指向我们内部数据变量的引用。我们可以将它们保持为隐藏和私有的。可以很恰当地认为这个返回值对象实质上是一个 我们模块的公有API。
这个返回值对象最终被赋值给外部变量 foo
,然后我们可以在这个API上访问那些属性,比如 foo.doSomething()
。
注意: 从我们的模块中返回一个实际的对象(字面量)不是必须的。我们可以仅仅直接返回一个内部函数。jQuery 就是一个很好地例子。jQuery
和 $
标识符是 jQuery “模块”的公有API,但是它们本身只是一个函数(这个函数本身可以有属性,因为所有的函数都是对象)。
doSomething()
和 doAnother()
函数拥有模块“实例”内部作用域的闭包(通过实际调用 CoolModule()
得到的)。当我们通过返回值对象的属性引用,将这些函数传送到词法作用域外部时,我们就建立好了可以观察和行使闭包的条件。
如果我们将模块放进一个自执行函数中,而且我们立即调用它,我们将模块放进一个 IIFE(见第三章)中,而且我们 立即调用它,可以实现某种“单例”:
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
复制代码
模块只是函数,所以它们可以接收参数:
function CoolModule(id) {
function identify() {
console.log(id);
}
return {
identify: identify
};
}
var foo1 = CoolModule("foo 1");
var foo2 = CoolModule("foo 2");
foo1.identify(); // "foo 1"
foo2.identify(); // "foo 2"
复制代码
3.2 现代的模块
各种模块依赖加载器/消息机制实质上都是将这种模块定义包装进一个友好的API。与其检视任意一个特定的库,不如让我 (仅)为了说明的目的 展示一个 非常简单 的概念证明:
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl, deps);
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
复制代码
这段代码的关键部分是 modules[name] = impl.apply(impl, deps)
。这为一个模块调用了它的定义的包装函数(传入所有依赖),并将返回值,也就是模块的API,存储到一个用名称追踪的内部模块列表中。
这里是我可能如何使用它来定义一个模块:
MyModules.define("bar", [], function () {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
});
MyModules.define("foo", ["bar"], function (bar) {
var hungry = "hippo";
function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome: awesome
};
});
var bar = MyModules.get("bar");
var foo = MyModules.get("foo");
console.log(
bar.hello("hippo")
); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
复制代码
模块“foo”和“bar”都使用一个返回公有API的函数来定义。“foo”甚至接收一个“bar”的实例作为依赖参数,并且可以因此使用它。换句话说,模块就是模块,即便你在它们上面放了一个友好的包装工具。
复习
闭包就是当一个函数即使是在它的词法作用域之外被调用时,也可以记住并访问它的词法作用域。
如果我们不能小心地识别它们和它们的工作方式,闭包可能会绊住我们,例如在循环中。但它们也是一种极其强大的工具,以各种形式开启了像 模块 这样的模式。
模块要求两个关键性质:
- 一个被调用的外部包装函数,来创建外围作用域。
- 这个包装函数的返回值必须包含至少一个内部函数的引用,这个函数才拥有包装函数内部作用域的闭包。