魔术师的幕后藏着一个人,我们将要揭开他的伪装
启示
JavaScript中闭包无处不在,你只需要能够识别并拥抱它。
闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意 识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿 来识别、拥抱和影响闭包的思维环境。
实质问题
一个闭包的直接定义,当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使喊你数是在当前词法作用域之外执行
以下的例子是闭包吗?
1 | function foo () { |
这是闭包吗?
技术上来讲,也许是。但根据前面的定义,确切地说并不是。我认为最准确地用来解释 bar() 对 a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却 是非常重要的一部分!)
以下代码清晰展示了闭包:
1 | function foo () { |
函数bar()的词法作用域能够访问foo()的内部作用域,然后将bar()函数本身当做一个值类型来进行传递。
在这个例子中,bar()函数在自己定义的词法作用域以外的地方执行。
闭包阻止了foo()的内存回收
引申出来的一点,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包
1 | (function foo() { |
把内部函数 baz 传递给 bar,当调用这个内部函数时(现在叫作 fn),它涵盖的 foo() 内部
作用域的闭包就可以观察到了,因为它能够访问 a。
也可以是间接的传递函数:
1 | var fn |
无论通过何种手段将内部函数传递到所在词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行 这个函数都会使用闭包。
闭包小结
本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一 级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!
循环和闭包
输出结果为5次6,延迟函数会在循环结束时才执行,即使将延时时间设为0,也会等待循环结束之后才执行。它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
以下方法是否可行?
1 | for (var i=1; i<=5; i++) { (function() { |
依然是输出5次6,这个IIFE只是一个什么都没有的空作用域,需要包含一点实质性内容才能为我们所用,它需要有自己的变量
1 | for (var i=1; i<=5; i++) { (function() { |
ok,再对IIFE进行改进!
1 | for (var i=1; i<=5; i++) { (function(j) { |
在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的
作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
重返块作用域
仔细思考我们对前面的解决方案,我们使用IIFe在每次迭代时都创建了一个新的作用域,换句话说,每次迭代我们都需要一个块作用域。 使用let,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。
本质上这是将一个块转换成一个可以被关闭的作用域
因此,上面的循环闭包可以写成:
1 | for (var i=1; i<=5; i++) { |
for 循环头部的 let 声明还会有一 个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随 后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。因此可以改写成:
1 | for (let i=1; i<=5; i++) { setTimeout( function timer() { |
模块
一个模块的例子:
1 | function CoolModult() { |
首先,CoolModule() 只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行 外部函数,内部作用域和闭包都无法被创建。
其次,CoolModule() 返回一个用对象字面量语法 { key: value, … } 来表示的对象。
引申出来的,模块模式需要具备两个必要条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
- 封闭函数必须返回至少一个内部函数,这样的内部函数才能在私有作用于中形成闭包,并且可以访问或者修改私有状态
改写为单例模式
1 | var foo = (function CoolModult() { |
将模块函数转换成了 IIFE(参见第 3 章),立即调用这个函数并将返回值直接赋值给 单例的模块实例标识符 foo。
现代的模块机制
1 | var MyModules = (function Manager() { |
这段代码的核心是modules[name] = impl.apply(impl, deps)。为了模块的定义引入了包装 函数(可以传入任何依赖),并且将返回值,也就是模块的 API,储存在一个根据名字来管 理的模块列表中。
未来的模块机制
1 | bar.js |
import 可以将一个模块中的一个或多个 API 导入到当前作用域中,并分别绑定在一个变量 上(在我们的例子里是 hello)。module 会将整个模块的 API 导入并绑定到一个变量上(在 我们的例子里是 foo 和 bar)。export 会将当前模块的一个标识符(变量、函数)导出为公 共 API。这些操作可以在模块定义中根据需要使用任意多次。
模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭 包模块一样。
小结
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。
模块有两个主要特征:
- 为创建内部作用域而调用了一个包装函数;
- 包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭 包。