子曰:最终的杀器-ES6 模块
概述
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。
1 | // ES6 模块 |
上面代码的实质是从fs
模块加载3 个方法,其他方法不加载。这种加载称为“编译时加载” 或者静态加载,即ES6 可用于在编译时就完成模块加载,效率要比CommonJS 模块的加载方式高。
由于ES6 模块是编译时加载,使得静态分析成为可能。
需要注意this
的限制。ES6 模块之后,顶层的this
指向undefined
,即不应该在顶层代码使用this
。
export 命令
模块功能主要由两个命令构成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。使用export
命令输出对外接口。
1 | // profile.js |
ES6 将profile.js
视为一个模块,export
命令对外输出了3 个变量。
export
的另一种写法。
1 | // profile.js |
接口名与模块内部变量之间,必须建立一一对应的关系
1 | // 报错 |
另外,export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到内部实时的值。
1 | export var foo = 'bar' |
上面代码输出变量foo
,值为bar
, 500 毫米之后变成baz
。
子曰:前面说了,import 就相当于将代码复制了过来,所以
setTimeout
的代码仍然可以影响foo 的值
import 命令
使用export
命令定义了模块的对外接口以后,其他JS 文件就可以通过import
命令加载这个模块。
1 | // main.js |
还可以使用as
关键字,将输入的变量重命名。
1 | import { lastName as surname } from './profile.js' |
注意:import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里,改写接口。
1 | import {a} from './xxx.js' |
由于import
是静态执行,所以不能使用表达式和变量,这些只能在运行时才能得到结果的语法结构。
1 | // 报错 |
模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(*
)指定一个对象,所有输出值都加载在这个对象上面。
1 | // circle.js |
现在加载这个模块。
1 | // math.js |
整体加载方法如下。
1 | import * as circle from './circle' |
export default 命令
使用export default
命令,为模块指定默认输出。
1 | // export-default.js |
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字。
1 | // import-default.js |
需要注意的是,这时import
命令后面,不使用大括号。
正因为export default
明in个其实是输入一个叫做default
的变量,所以它后面不能跟变量声明语句。
1 | // 正确 |
因为export default
命令的本质是将后民安的值,赋给default
变量,所以可以直接将一个值写在export default
之后。
1 | // 正确 |
export default
也可以用来输出类。
1 | // MyClass.js |
export 与import 的复合写法
如果在一个模块中,先输入后输出一个模块,import
语句可以与export
语句写在一起。
1 | export { foo, bar } from 'my_module' |
需要注意的是,写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
跨模块常量
const
声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。
1 | // constants.js 模块 |
如果要使用的变量非常多,可以建立一个专门的constants
目录,将各种变量写在不同的文件里面。
1 | // constants/db.js |
然后,将这些文件输出的常量,合并在index.js
里面。
1 | // constants/index.js |
使用的时候,直接加载index.js
就可以了
1 | // script.js |
import()
简介
import
命令会被JavaScript 引擎静态分析,先于模块内的其他语句执行(import
命令叫做“连接binding” 其实更合适)。
1 | // 报错 |
上面代码中,引擎处理import
是在编译时,这是不会去分析或执行if
语句,所以import
语句放在if
语句块之中毫无意义,因此会报语法错误,而不是执行错误。也就是说,import
和export
命令只能在模块的顶层,不能在代码块之中。
因此,有一个提案,建议引入import()
函数,完成动态加载。
1 | import(specifier) |
上面代码中,import
函数的参数specifier
,指定所要加载的模块的位置。import
命令能够接受什么参数,import()
函数就能接受什么参数,两者的主要区别在于后者是动态加载。
import()
返回一个Promise 对象。
1 | const main = document.querySelector('main') |
import()
函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。- 另外,
import()
函数与所加载的模块没有静态连接关系,这点也是与import
命令不同。 import()
类似于Node 的require
方法,区别主要是前者是异步加载,后者是同步加载。
注意点
import()
加载模块成功以后,这个模块会作为一个对象,当作then
方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
1 | import('./myModule.js') |
如果想同时加载多个模块,可以采用下面的写法。
1 | Promise.all([ |
import()
也可以用在async
函数之中。
1 | async function main() { |