历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

  1. // CommonJS模块
  2. let { stat, exists, readFile } = require('fs');
  3. // 等同于
  4. let _fs = require('fs');
  5. let stat = _fs.stat;
  6. let exists = _fs.exists;
  7. let readfile = _fs.readfile;

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

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

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
除了静态加载带来的各种好处,ES6 模块还有以下好处。

  • 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

本章介绍 ES6 模块的语法,下一章介绍如何在浏览器和 Node 之中,加载 ES6 模块。

export 命令

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

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

上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。
export的写法,除了像上面这样,还有另外一种。

  1. // profile.js
  2. var firstName = 'Michael';
  3. var lastName = 'Jackson';
  4. var year = 1958;
  5. export { firstName, lastName, year };

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

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

上面代码对外输出一个函数multiply
通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

  1. function v1() { ... }
  2. function v2() { ... }
  3. export {
  4. v1 as streamV1,
  5. v2 as streamV2,
  6. v2 as streamLatestVersion
  7. };

上面代码使用as关键字,重命名了函数v1v2的对外接口。重命名后,v2可以用不同的名字输出两次。
需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

  1. // 报错
  2. export 1;
  3. // 报错
  4. var m = 1;
  5. export m;

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

  1. // 写法一
  2. export var m = 1;
  3. // 写法二
  4. var m = 1;
  5. export {m};
  6. // 写法三
  7. var n = 1;
  8. export {n as m};

上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。
同样的,functionclass的输出,也必须遵守这样的写法。

  1. // 报错
  2. function f() {}
  3. export f;
  4. // 正确
  5. export function f() {};
  6. // 正确
  7. function f() {}
  8. export {f};

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

(3)yield*
yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

  1. let generator = function* () {
  2. yield 1;
  3. yield* [2,3,4];
  4. yield 5;
  5. };
  6. var iterator = generator();
  7. iterator.next() // { value: 1, done: false }
  8. iterator.next() // { value: 2, done: false }
  9. iterator.next() // { value: 3, done: false }
  10. iterator.next() // { value: 4, done: false }
  11. iterator.next() // { value: 5, done: false }
  12. iterator.next() // { value: undefined, done: true }

与其他遍历语法的比较

以数组为例,JavaScript 提供多种遍历语法。最原始的写法就是for循环。

  1. for (var index = 0; index < myArray.length; index++) {
  2. console.log(myArray[index]);
  3. }

这种写法比较麻烦,因此数组提供内置的forEach方法。

  1. myArray.forEach(function (value) {
  2. console.log(value);
  3. });

这种写法的问题在于,无法中途跳出forEach循环,break命令或return命令都不能奏效。
for...in循环可以遍历数组的键名。

  1. for (var index in myArray) {
  2. console.log(myArray[index]);
  3. }

for...in循环有几个缺点。

  • 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
  • for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
  • 某些情况下,for...in循环会以任意顺序遍历键名。

总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。
for...of循环相比上面几种做法,有一些显著的优点。

  1. for (let value of myArray) {
  2. console.log(value);
  3. }
  • 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
  • 不同于forEach方法,它可以与breakcontinuereturn配合使用。
  • 提供了遍历所有数据结构的统一操作接口。

下面是一个使用 break 语句,跳出for...of循环的例子。

  1. for (var n of fibonacci) {
  2. if (n > 1000)
  3. break;
  4. console.log(n);
  5. }

上面的例子,会输出斐波纳契数列小于等于 1000 的项。如果当前项大于 1000,就会使用break语句跳出for...of循环。