Skip to content

模块Module

https://es6.ruanyifeng.com/#docs/module

概述

历史上 JavaScript 一直没有模块 (module) 体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。
其他语言都有这项功能,比如 Ruby 的 require、Python 的 import,甚至连 CSS 都 @import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍

在 ES6 之前,社区指定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。
前者用于服务器,后者用于浏览器。
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
CommonJS 和 AMD 模块,都只能在运行时确定这些东西。
比如,CommonJS 模块就是对象,输入时必须查找对象属性

1
2
3
4
5
6
7
8
// CommonJS 模块
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

上面代码的实质是整体加载 fs 模块(即加载 fs 的所有方法),生成一个对象(_fs),然后再从这个对象上读取 3 个方法。
这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”

ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,再通过 import 命令输入

1
2
// ES6 模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从 fs 模块加载 3 个方法,其他方法不加载。
这种加载称为 “编译时加载” 或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。
当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

由于 ES6 模块是编译时加载,使得静态分析成为可能。
有了它,就能进一步拓宽 JavaSCript 的语法,比如引入宏(macro) 和类型检验(type system) 这些只能靠静态分析实现的功能

export 命令

模块功能主要由两个命令构成: exportimport
export 命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。
如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量。
下面是一个 JS 文件,里面使用 export 命令输出变量

1
2
3
4
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jacksom';
export var year = 1958;

上面代码是 profile.js 文件,保存了用户信息。ES6 将其视为一个模块,里面用 export 命令对外输出了三个变量

export 的写法,除了像上面这样,还有另外一种

1
2
3
4
5
6
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };

上面代码 export 命令后面,使用大括号指定所要输出的一组变量。
它与前一种写法(直接放置在 var 语句前)是等价的,但是应该优先考虑使用这种写法。
因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量

export 命令除了输出命令,还可以输出函数或类(class)

1
2
3
export function multiply(x, y) {
    return x * y;
};

上面代码对外输出一个函数 multiply

通常情况下,export 输出的变量就是本来的名字,但是可以使用 as 关键字重命名

1
2
3
4
5
6
7
8
function v1() { ... }
function v2() { ... }

export {
    v1 as streamV1,
    v2 as streamV2,
    v2 as streamLatestVersion
};

上面代码使用 as 关键字,重命名了函数 v1 和 v2 的对外接口。重命名后,v2 可以用不同的名字输出两次

需要特别注意的是,export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系

1
2
3
4
5
6
// 报错
export 1;

// 报错
var m = 1;
export m;

上面两种写法都会报错,因为没有提供对外的接口。
第一种写法直接输出 1,第二种写法通过变量 m,还是直接输出 1.
1 只是一个值,不是接口。正确的写法是下面这样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 写法一
export var m = 1;

// 写法二 
var m = 1;
export { m };

// 写法三
var n = 1;
export { n as m };

上面三种写法都是正确的,规定了对外的接口 m。
其他脚本可以通过这个接口,取到值 1.它们的实质是,在接口名与模块内部变量之间,建立了意义对应的关系

同样的,function 和 class 的输出,也必须遵守这样的写法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 报错
function f() {}
export f;

// 正确
export function f() {};

// 正确
function f() {}
export {f};

另外,export 语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值

1
2
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

上面代码输出变量 foo,值为 bar,500 毫秒之后变成 baz

最后, export 命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错

import 命令

使用 export 命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import 命令加载这个模块

1
2
3
4
5
6
// main.js
import { firstName, lastName, year } from './profile.js';

function setName(element) {
    element.textContent = firstName + ' ' + lastName;
}

上面代码的 import 命令,用于加载 profile.js 文件,并从中输入变量。
import 命令接受一对大括号,里面指定要从其他模块导入的变量名。
大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同