JavaScript中闭包无处不在,我们只需要去识别并接受它。当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
闭包是什么样的?
可以来看一段代码,清晰的展示闭包:
1 | function foo() { |
函数bar()
的词法作用域能够访问foo()
的内部作用域,然后我们将函数bar()
本身作为一个值类型进行传递,将bar所引用的函数对象本身当做返回值。
在foo()
执行后,其返回值(也就是内部的bar()
函数)赋值给变量baz
,并调用baz()
,实际上时通过不同的标识符引用调用了内部的函数bar()
。
bar()
虽然可以正常运行,但是它在定义自己的词法作用域以外的地方执行。
在foo()
执行后,通常会期待foo()
的整个内部作用域都被销毁,由于看上去foo()
的内容不会再被使用,所以很自然地会考虑对其进行回收。然而闭包神奇支出正是可以阻止这件事情的发生。事实上内部作用域依旧存在,因此并没有被回收。可以发现,是bar()
本身在使用这个内部作用域。
由于bar()
声明于foo()
的内部,所以bar()
拥有foo()
内部作用域的闭包,使得该作用域能够一直存活,以供bar()
在之后任何时间进行引用。bar()
依然持有对该作用域的引用,而这个引用就叫做“闭包”。
因此,在baz
变量被实际调用的时候,它访问到了定义bar()
时的词法作用域,因此它可以如预期般访问变量a
。即,闭包使函数可以继续访问定义时的词法作用域。
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时,都可以观察到闭包。
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义域的引用,无论在何处执行这个函数都会使用闭包。
我们来看一个常用的延时函数:
1 | function wait(message) { |
将一个内部函数timer()
传递给setTimeout()
。timer
具有涵盖wait()
作用域的闭包,因此还保有对变量message
的引用。
深入分析下,内置的工具函数setTimeout()
持有对一个参数的引用,引擎会调用传递过去的这个函数,而词法作用域在这个过程中保持完整,这就是闭包。
for循环和闭包
再看for循环。我们前面看了let这个关键字可以来劫持块作用域,并且在这个块作用域中声明一个变量。这样我们可以用let和for结合来实现一些酷酷的东西。let声明的变量不止被声明一次,每次迭代都会声明,随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
1 | for (let i = 0; i < 6 ; i ++) { |
这样我们可以就正确的延时输出1,2,3,4,5,6了。
模块
有很多的代码模式利用闭包的强大威力,但从表面上看似乎与回调无关,模块就是其中一种。
先来看一个简单的模块:
1 | function FooModule() { |
这种模式在JavaScript中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露。
我们来分析一下上面这个FooModule模块。
- 首先,它是一个函数,必须要调用它才能创建一个模块实例,否则内部作用域和闭包都无法创建。
- 其次,它返回一个用对象字面量语法
{key: value}
来表示的对象。这个返回的对象中含有对内部函数的引用,这样可以保持内部数据变量是隐藏且私有状态。 - 这个对象最终被赋值给外部的变量
foo
,然后就可以通过foo
来访问模块中的属性方法。
返回一个实际的对象不是必须的,也可以直接返回一个内部的函数。
简单来讲,模块模式需要具备两个必须条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有状态。
现代的模块机制
大多数模块加载器/管理器本质上都是将这种模块定义分装进一个友好的API,例如简单的一个模块依赖实现:
1 | var MyModles = (function Manager() { |
未来的模块机制
ES6中为模块增加了一级语法支持。当通过模块系统进行加载时,ES6会将文件作为独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员。使用关键字export
和import
来导出和导入一个模块。