启示
闭包是基于词法作用域书写时代码所产生的自然结果。
闭包的创建和使用在你的代码中随处可见。
实质问题
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
1 | function foo(){ |
这不是一个纯粹的闭包,bar()
对a的引用是词法作用域的查找规则(RHS),只是闭包的一部分。
1 | function foo(){ |
foo()
执行后,通常其整个内部作用域都会被销毁,但实际上foo()
内部作用域依然存在,被嵌套在foo函数内的bar()
引用,这个引用就叫做闭包。
现在我懂了
本质上,无论何时何地,如果将(访问他们各自词法作用域的)函数当作第一级的值类型到处传递,你就会看到闭包在这些函数中的应用。
定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包。
循环和闭包
1 | for(var i=1;i<=5;i++){ |
预期是,分别输出1~5,每秒一次,每次一个;
实际上,每秒一次的频率输出6次。
尽管循环中的五个函数在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。
解决方法:
1 | for(var i=1;i<=5;i++){ |
在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
重返块作用域
迭代内使用IIFE会为每个迭代都生成一个新的作用域==每次迭代需要一个块作用域。本质上,将一个块转换成一个可以被关闭的作用域。
1 | for(var i=1;i<=5;i++){ |
for
循环头部的let
声明有一个特殊的行为:
变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
1 | for(let i=1;i<=5;i++){ |
模块
1 | function CoolModule() { |
可以将返回的对象类型的值看作本质上的模块的公共API。
模块模式需要具备两个必要条件:
- 必须有外部的封闭函数,该函数必须至少调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有状态。
当只需要一个实例时,对模式改进成单例模式:
1 | var foo = (function CoolModule() { |
模块也是普通函数,也可以接受函数。
模块另一个强大的用法:修改将要作为公共API返回的对象。
通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改。
1 | var foo=(function coolModule(id){ |
现代的模块机制
大多数模块依赖加载器/管理器本质上都是将这种模块定义封装到一个友好的API。
1 | var myModule=(function(){ |
为了模块的定义引入了包装函数(可以传入任何依赖)modules[name]=impl.apply(impl,deps)
,并且将返回值,也就是模块API,储存在一个根据名字来管理的模块列表中。