Javascript 笔记:模块
这篇文章记录 JS 模块化相关的概念。
背景
随着 ES6 的出现,JS 模块已经成为正式的标准了,它的设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
ES6 模块不是对象,而是通过 export
命令显式指定输出的代码,输入时也采用 import
命令导入。
模块
模块功能主要由两个命令构成:export
和
import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用
export
关键字输出该变量。
export 命令
export
命令能够对外输出的就是三种接口:函数(Functions),
类(Classes),var、let、const 声明的变量(Variables)。
输出变量
1 | // profile.js |
上面代码对外输出三个变量。
输出类
1 | // 导出类 |
上面代码对外输出一个类。
输出函数
1 | export function multiply(x, y) { |
上面代码对外输出一个函数multiply。
关键字重命名
通常情况下,export
输出的变量就是本来的名字,但是可以使用 as
关键字重命名。
1 | function v1() { ... } |
上面代码使用 as
关键字,重命名了函数 v1 和 v2
的对外接口。重命名后,v2 可以用不同的名字输出两次。
输出变量而不是值
需要特别注意的是,export
命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
1 | // 报错 |
上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量 m,还是直接输出 1。1 只是一个值,不是接口。正确的写法是下面这样。
1 | // 写法一 |
上面三种写法都是正确的,规定了对外的接口 m。其他脚本可以通过这个接口,取到值 1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。
import 命令
使用 export
命令定义了模块的对外接口以后,其他 JS
文件就可以通过 import
命令加载这个模块。
1 | // main.js |
上面代码的 import
命令,用于加载 profile.js
文件,并从中输入变量。import
命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。
变量重命名
如果想为输入的变量重新取一个名字,import
命令要使用
as
关键字,将输入的变量重命名。
1 | import { lastName as surname } from './profile.js'; |
只读变量
import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
1
2
3import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
上面代码中,脚本加载了变量 a
,对其重新赋值就会报错,因为
a
是一个只读的接口。但是,如果 a
是一个对象,改写 a
的属性是允许的。
1 | import {a} from './xxx.js' |
上面代码中,a
的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。
模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(*
)指定一个对象,所有输出值都加载在这个对象上面。
下面是一个 circle.js
文件,它输出两个方法
area
和 circumference
。
1 | // circle.js |
现在,加载这个模块。
1 | // main.js |
上面写法是逐一指定要加载的方法,整体加载的写法如下。
1 | import * as circle from './circle'; |
注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。
1 | import * as circle from './circle'; |
export default 命令
从前面的例子可以看出,使用 import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到
export default
命令,为模块指定默认输出。
1 | // export-default.js |
上面代码是一个模块文件
export-default.js
,它的默认输出是一个函数。
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字。
1 | // import-default.js |
上面代码的 import
命令,可以用任意名称指向
export-default.js
输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时
import
命令后面,不使用大括号。
export default
命令用在非匿名函数前,也是可以的。
1 | // export-default.js |
上面代码中,foo
函数的函数名
foo
,在模块外部是无效的。加载的时候,视同匿名函数加载。
下面比较一下默认输出和正常输出:
1 | // 第一组 |
上面代码的两组写法,第一组是使用 export default
时,对应的 import
语句不需要使用大括号;第二组是不使用
export default
时,对应的 import
语句需要使用大括号。
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此
export default
命令只能使用一次。所以,import
命令后面才不用加大括号,因为只可能唯一对应 export default
命令。
本质上,export default
就是输出一个叫做
default
的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。
1 | // modules.js |
正是因为 export default
命令其实只是输出一个叫做
default
的变量,所以它后面不能跟变量声明语句。
1 | // 正确 |
上面代码中,export default a
的含义是将变量
a
的值赋给变量
default
。所以,最后一种写法会报错。
同样地,因为 export default
命令的本质是将后面的值,赋给
default
变量,所以可以直接将一个值写在
export default
之后。
export default
也可以用来输出类: 1
2
3
4
5
6// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
参考: