5.1 闭包
- 当函数可以记住并访问所在的词法作用域,即使函数名是在当前词法作用域之外执行,这时就产生了闭包。
1 | function foo() { |
bar()
在自己定义的词法作用域以外的地方执行。
bar()
拥有覆盖foo()
内部作用域的闭包,使得该作用域能够一直存活,以供bar()
在之后任何时间进行引用,不会被垃圾回收器回收
bar()
持有对foo()
内部作用域的引用,这个引用就叫做闭包。
1 | // 对函数类型的值进行传递 |
- 把内部函数
baz
传递给bar
,当调用这个内部函数时(现在叫做fn
),它覆盖的foo()
内部作用域的闭包就形成了,因为它能够访问a。
1 | // 间接的传递函数 |
- 将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
1 | function wait(message) { |
- 在引擎内部,内置的工具函数
setTimeout(..)
持有对一个参数的引用,这里参数叫做timer,引擎会调用这个函数,而词法作用域在这个过程中保持完整。这就是闭包 - 定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
1 | // 典型的闭包例子:IIFE |
5.2 循环和闭包
1 | for (var i = 1; i <= 5; i++) { |
- 延迟函数的回调会在循环结束时才执行,输出显示的是循环结束时
i
的最终值。 - 尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个
i
尝试方案1:使用IIFE增加更多的闭包作用域
1 | for (var i = 1; i <= 5; i++) { |
尝试方案2:IIFE增加变量
1 | for (var i = 1; i <= 5; i++) { |
尝试方案3:改进型,将i
作为参数传递给IIFE函数
1 | for (var i = 1; i <= 5; i++) { |
5.2.1 块作用域和闭包
let
可以用来劫持块作用域,并且在这个块作用域中声明一个变量。- 本质上这是将一个块转换成一个可以被关闭的作用域
1 | for (var i = 1; i <= 5; i++) { |
for
循环头部的let
声明会有一个特殊的行为。变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
上面这句话参照3.4.3–—2.let循环,即以下
1 | { |
循环改进:
1 | for (let i = 1; i <= 5; i++) { |
5.3 模块
模块模式需要具备两个必要条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例,可以通过IIFE实现单例模式)
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
1 | function CoolModule() { |
- 使用IIFE实现单例模式
立即调用这个函数并将返回值直接赋予给单例的模块标识符foo。
1 | var foo = (function CoolModule() { |
5.5.1 现代的模块机制
大多数模块依赖加载器/管理器本质上是将这种模块定义封装进一个友好的API。
1 | var MyModules = (function Manager() { |
使用上面的函数来定义模块:
1 | MyModules.define( "bar", [], function() { |
5.5.2 未来的模块机制
在通过模块系统进行加载时,ES6会将文件当做独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样可以导出自己的API成员。
ES6模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)
- 基于函数的模块不能被静态识别(编译器无法识别),只有在运行时才会考虑API语义,因此可以在运行时修改一个模块的API。
- ES6模块API是静态的(API模块不会在运行时改变),会在编译期检查对导入模块的API成员的引用是否真实存在。
1 | // bar.js |
import
:将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上module
:将整个模块的API导入并绑定到一个变量上。export
:将当前模块的一个标识符(变量、函数)导出为公共API