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

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

CommonJS(运行时加载)

CommonJS模块:

let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
1
2
3
4
5
6
7

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

ES6模块化(编译时加载)

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

ES6模块:

import {stat, exists, readFile} from 'fs';
1

上面代码的实质是从fs模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即ES6可以在编译时就完成模块加载,效率要比CommonJS模块的加载方式高。

特别说明:ES6的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

export命令

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

// 写法一
export var m = 1;

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

// 写法三
var n = 1;
// 导出重命名
export {n as m};
1
2
3
4
5
6
7
8
9
10
11

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

export的值是动态绑定的

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

export let foo = 'bar';
setTimeout(() => foo = 'baz', 500);
1
2
// main.js
import {foo} from './test2';

console.log(foo);
setTimeout(() => console.log(foo), 1000);
1
2
3
4
5

执行:

node -r @babel/register main.js
1

上面代码输出变量foo,值为bar,500 毫秒之后变成baz。 6bb1ada3bd3564bdff3c0532e6324fb3.png 这一点与CommonJS规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新。

import 命令

import { surname } from './profile.js';
// 为输入的变量重新取一个名字
import { lastName as surname } from './profile.js';
1
2
3

模块的整体加载

用星号*指定一个对象,所有输出值都加载在这个对象上面。

// circle.js
export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}
1
2
3
4
5
6
7
8

加载上述模块:

// main.js
// 逐一指定要加载的方法
import { area, circumference } from './circle';

console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));
1
2
3
4
5
6

上面写法是逐一指定要加载的方法,整体加载的写法如下:

import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
1
2
3
4

export default命令

需要注意:export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此,export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。

本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同于
// export default add;

// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
1
2
3
4
5
6
7
8
9
10
11
12

正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。

// 正确
export var a = 1;

// 正确
var a = 1;
export default a;

// 错误
export default var a = 1;
1
2
3
4
5
6
7
8
9

上面代码中,export default a的含义是将变量a的值赋给变量default。所以,最后一种写法会报错。

同样地,因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

// 正确
export default 42;

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

上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为default。

有了export default命令,输入模块时就非常直观了,以输入 lodash 模块为例。

import _ from 'lodash';
1

如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。

import _, { each, forEach } from 'lodash';
1

demo

// main.js
import * as Test from './index';
import {Fn, Add} from './index';

console.log(Fn);
console.log(Add);
console.log(Test);
1
2
3
4
5
6
7
// index.js
// export {default as Add} from './test';
// export {default as Fn} from './export-default';
// 等价于下面的写法
import Add from './test';
import Fn from './export-default';

export {
    Add,
    Fn
}
1
2
3
4
5
6
7
8
9
10
11
// test.js
const add = (x, y) => x + y;

export default add;
1
2
3
4
// export-default.js
const fn = (x, y) => x * y;

export default fn;
1
2
3
4

572d1694c339448b146fdcda40b88cb4.png

demo

npm install @babel/core @babel/preset-env @babel/register -D
1

babel.config.js

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: {
          node: "current" // 针对当前node版本进行编译,删除该行可能导致`npm start`报错
        }
      }
    ]
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node -r @babel/register index.js"
  }
1
2
3
4

export default和export区别

  • export与export default均可用于导出常量、函数、文件、模块等;
  • 可以在其它文件或模块中通过import+(常量 | 函数 | 文件 | 模块)名的方式将其导入,以便能够对其进行使用;
  • 在一个文件或模块中,export、import可以有多个,export default仅有一个;
  • 通过export方式导出,在导入时要加{},export default则不需要。
  • export方式(命名导出)导出的话,在模块或者变量引入的时候需要知道对应的模块或者变量的名称,而export default则不需要,可以随意起名。
  • export default:默认导出,引入的时候不需要指定特定的名字。
  • export:命名导出,导出和引入的变量名称需要一致。
// 1.export
//a.js
export const str = "blablabla~";
export function log(sth) {
  return sth;
}
对应的导入方式:

//b.js
import { str, log } from 'a'; // 也可以分开写两次,导入的时候带花括号

// 2.export default
//a.js
const str = "blablabla~";
export default str;
// 对应的导入方式:

//b.js
import str from 'a'; //导入的时候没有花括号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

使用export default命令,为模块指定默认输出,这样就不需要知道所要加载模块的变量名。

//a.js
let sex = "boy";
export default sex;(sex不能加大括号)
1
2
3

原本直接export sex外部是无法识别的,加上default就可以了。但是一个文件内最多只能有一个export default。 其实此处相当于为sex变量值"boy"起了一个系统默认的变量名default,自然default只能有一个值,所以一个文件内不能有多个export default

// b.js
import any from "./a.js";
import any12 from "./a.js";
console.log(any, any12); // boy, boy
1
2
3
4

本质上,a.js文件的export default输出一个叫做default的变量,然后系统允许你为它取任意名字。所以可以为import的模块起任何变量名,且不需要用大括号包含