1、概述
在 ES6 之前,社区制定了一些模块加载方法,最主要的有 CommonJS 和 AMD 两种,前者用于服务器,后者用于浏览器。 ES6 在语言层面上,实现了模块功能,而且实现相当简单,完全可以取代 CommonJS 和 AMD 规范,称为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想,是尽量的模块化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块是对象,输入时必须查找对象属性,并且加载时是先加载整个模块对象,然后获取对象上的方法,这种加载方式称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没有办法在编译时做“静态优化。
ES6 模块不是对象,而是通过 export
命令显式指定输出的代码,再通过 import
命令输入。这导致了没法引用 ES6 模块本身,因为它不是对象。
ES6 模块的好处:
- ES6 模块加载只加载需要的方法,其他方法不加载,这种加载称为编译时加载或静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块加载的方式高。
- 不再需要 UMD 模块格式,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
- 将来浏览器的新 API 就能用模块格式提供,不再必须做出全局变量或者 navigator 对象的属性。
- 不再需要对象作为命名空间(比如 Math 对象),未来这些功能可以通过模块提供。
2、export 命令
模块功能主要由两个命令构成:export 和 import 。export 命令用于规定模块的对外接口, import 命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件,该文件内部的所有变量(或函数、类),外部无法获取。如果你希望外部能够读取模块内部的某个变量(或函数、类),就必须使用 export 关键字输出该变量(或函数、类)。
使用 export 输出变量、函数、类,有两方式:
将 export 直接放在
var
、function
、class
语句前export var firstName = 'Michael' export function foo(){}
将 export 放在最顶层环境中 使用大括号指定所有要输出的一组变量(或函数、类)。
var firstName = 'michael'; var lastName = 'Jackson'; function foo(){} export {firstName, lastName, foo}
在使用第二种方式的时候可以使用 as
关键字重命名。重命名后,变量(或函数、类)可以用不同的名字输出两次。
export {
foo as foo1,
foo as foo2
}
注意:
(1)export
命令规定的是对外的接口,必须与模块内部的变量(或函数、类)建立一种一一对应关系。(使用第二种方式的时候必须使用大括号,否则报错)
// 以下全部会报错
export 1; // 直接输出 1
export {1}; // 不符合命名规范
var m = 1;
export m; // 通过变量 m ,还是直接输出 1
function f(){}
export f;
(2)export
语句输出的接口,与其对应的值是动态绑定的关系,即通过该接口,可以取到模块内部实时的值。
export var a = 'bar';
setTimeout(() => a = 'baz', 500);
上面代码输出变量 a ,值为 bar ,500 毫秒后变成 baz。
(3)export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。
function f(){
export var a = 1;
// 报错 Syntax error: 'import' and 'export' may only appear at the top level
}
3、import 命令
使用 export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import
命令加载这个模块。
import
命令接受一对大括号,里面指定要从其他模块导入的变量(或函数、类),大括号里面的接口名,必须与被导入模块对外接口的名称相同,即 import
的名称与 export
的名称要相同。在 import
命令中可以使用 as 关键字给接口重命名。
import
后面的 from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,并且 .js
后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件告诉 JS 引擎模块的位置。
import {firtsName, lastName as lN } from './test.js'
注意:
(1)import
命令有提升效果,会提升到整个模块的头部,首先执行。
foo();
import {foo} from './test';
(2)由于 import 是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 以下语句会报错
import {'f' + 'oo'} from './test';
let module = './test';
import {foo} from module;
if( true ){
import {foo} from './test';
}
(3)import
语句会执行所加载的模块,并且只执行一次。因此可以有下面的写法。
import 'load';
import 'load';
import {foo} from './test';
import {bar} from './test';
=> import {foo, bar} from './test';
上述代码加载了两次 load
,但是这会执行一次。
(4)import
导入的变量(或函数、类)应该是可以静态分析的,所以不允许运行时改变。
import {foo} from './test';
foo = 'bar'; // 报错,Syntax error: "foo" is read-only
(5)除了指定加载某个输出值,还可以使用整体加载,即使用星号(*
)指定一个对象,所有输出值都加载在这个对象上面。
import * as all from './test';
使用时: all.foo
4、export default 命令
使用 import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,export default
命令为模块指定默认输出。
export default function(){} // test.js 中指定默认输出一个函数
import foo from './test'; // 加载模块时,import 为该匿名函数指定任意名字
foo();
注意:
(1)使用 export default
输出的默认变量(或函数、类),在加载时,import
后面不使用大括号。
(2)export default
命令也可以导出非匿名函数。但是非匿名函数的名称只有在模块内部有效。在加载额时候,视同匿名函数加载。
export default function foo(){}
或
function foo(){}
export default foo;
(3)使用 export default
时,对应的 import
语句不能使用大括号,否则会报错;使用 export
时,对应的 import
语句必须使用大括号,否则会报错。
(4)export default
命令用于指定模块的默认输出,显然一个模块只能有一个默认输出,因此, export default
只能使用一次,否则会报错。
(5)export default
本质上是输出一个叫做 default
的变量或者方法,然后在加载时指定任意名称。
function foo(){}
export {foo as default}; => export default foo;
import xxx from './t'; => import { default as xxx} from './t';
(6)export default
命令实际输出的一个 default
的变量,所以他后面不能跟变量声明语句。
export var a = 1; // 正确
var a = 1;
export default a; // 正确
export default var a = 1; // 错误
(7)export default
命令实际上是将后面的值赋给 default
并输出,所以可以直接将一个值写在export default
之后。
export default 4; // 正确
export 4; // 错误,因为没有指定对外的接口,而上一句指定了 default 为对外接口
(8)可以在 export default
命令中同时输入默认方法和其他方法。
import _, { each, each as forEach } from 'lodash';
(9)export default
可以用来输出类。
5、export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import 语句可以与 export 语句写在一起。
export {foo. bar} from './test';
// 等同于
import {foo, bar} from './test';
export {foo, bar};
export {foo as myFoo} from './test'; // 接口改名
export * from './test'; // 整体输出(忽略 test 的 default 方法)
export {default} from 'foo'; // 默认接口
export {foo as default} from './test'; // 具名接口改为默认接口
export {default as foo} from 'foo'; // 默认接口改为具名接口
以下三种 import 语句没有复合写法。
import * as someIdentifier from 'someModule';
import someIdentifier from 'someModule';
import someIdentifier, {nameIdentifier} from 'someModule';
6、模块的继承
模块之间也可以继承。在子模块中将父模块先输入后输出,同时在扩展模块的方法即可。
// super.js
export function foo(){}
// sub.js
export * from './super';
export function bar(){}
上述代码中,sub
继承了 super
的 foo
方法,并定义了自己的 bar
方法,使用时加载 sub.js
就可得到 foo
和 bar
方法。
7、跨域常量
const 声明的常量只在当前代码块有效。如果设置跨域模块的常量(即跨多个文件),或者说一个值被多个模块共享,可以采用 export
将其输出出去。