详细信息请前往:阮一峰-es6-module-写得很好。
概述
在ES6之前,社区开发了一些模块加载解决方案,其中最重要的是CommonJS和AMD。第一个用于服务器,第二个用于浏览器。 ES6在语言标准层面实现了模块功能,而且实现相当简单。它可以完全取代CommonJS和AMD规范,成为浏览器和服务器的通用模块解决方案。
ES6 模块被设计为尽可能静态,因此可以在编译时指定模块依赖关系以及输入和输出变量。 CommonJS 和 AMD 模块都只能在运行时确定这些事情。例如,CommonJS 模块是对象,需要在输入时查找对象属性。
// 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
命令进行输入的代码。
// ES6模块
import { stat, exists, readFile } from 'fs';
上面代码的本质是从fs模块加载3个方法,而不是加载其他方法。这种加载称为“编译时加载”或静态加载,即ES6可以在编译时完成模块的加载,比CommonJS的模块加载效率更高。当然,这也使得无法引用 ES6 模块本身,因为它不是一个对象。
由于 ES6 模块是在编译时加载的,因此可以进行静态分析。有了它,JavaScript 语法可以进一步扩展,例如引入宏和类型系统等只能通过静态分析才能实现的功能。
严格模式
ES6 模块自动采用严格模式,无论您是否添加“use strict”;在模块头中。
严格模式主要有以下限制:
- 变量必须在使用前声明
- 函数参数不能有同名属性,否则会报错
- 不能与命令
with
一起使用
- 不能给只读属性赋值,否则会报错
- 前缀0不能作为八进制数,否则会报错
- 不能删除不可删除的属性,否则会报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
eval
不在其外部作用域中引入变量eval
和arguments
无法重新排列arguments
不会自动反映功能参数的更改- 无法使用
arguments.callee
- 无法使用
arguments.caller
- 不允许
this
指向全局对象 - 无法使用
fn.caller
和fn.arguments
获取函数调用堆栈 - 添加保留字(如
protected
、static
和interface
)
出口
模块的功能主要由两个命令组成:export
和import
。 export
命令用于指定模块对外接口,import
命令用于指定其他模块提供的功能。
该模块是一个独立的文件。文件内的所有变量都无法从外部访问。如果希望外部能够读取模块内部的变量,则必须使用export
关键字来输出该变量。下面是一个使用export
命令输出变量的JS文件。
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
上面的代码是一个存储用户信息的文件profile.js
。 ES6 将此视为一个使用 export
语句输出三个变量的模块。
export
。
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };
上面的代码位于导出语句之后,并使用大括号指定要输出的变量集。它相当于前一个(直接放在 var 命令之前),但应优先使用。因为这样你就能一目了然地看到脚本末尾写了哪些变量。
除了输出变量之外,导出命令还可以显示函数或类。
export function multiply(x, y) {
return x * y;
};
上面的代码显示了函数multiply
。
通常,使用 export
的变量输出是其原始名称,但可以使用 as
关键字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
上面的代码使用as关键字来重命名函数v1和v2的外部接口。重命名后,v2可能会以不同的名称输出两次。
需要注意的是,导出命令指定了一个外部接口,并且必须与模块内部的变量进行单独匹配。
// 报错
export 1;
// 报错
var m = 1;
export m;
以上两种写法都会因为没有外部接口可用而报错。第一种写法直接输出1,第二种写法通过变量m直接输出1。1只是一个值,不是一个接口。正确的表示方法如下。
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
以上三种表示方式都是正确的,指定了外部接口m,其他脚本可以通过该接口获取值1。它们的本质是创建接口名称和模块内部变量之间的相互对应关系。
同样,函数和类的输出也必须符合这种表示方法。
// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};
另外,export
命令的接口输出与对应的值具有动态绑定关系,即可以通过该接口获取模块内部的实时值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面的代码打印变量foo
,值为 bar,500 毫秒后更改为 baz
。
这与 CommonJS 规范完全不同。 CommonJS 模块打印值缓存,并且不会发生动态更新,详细信息请参阅下面的《Module 的加载实现》。
最后,export
语句可以出现在模块中的任何位置,只要它位于模块的顶层即可。如果是在块级范围内,就会报错,下一节的import
语句也会报错。这是因为静态优化不能在条件代码块内执行,这违背了 ES6 模块的最初设计意图。
function foo() {
export default 'bar' // SyntaxError
}
foo()
上面代码中,函数中放置了命令export
,就会报错。
导入
使用export
命令定义模块对外接口后,其他JS文件就可以使用import
命令加载该模块。
上述代码中的import
命令用于加载文件profile.js
并从中输入变量。 import
命令接受一对花括号,指定要从其他模块导入的变量名称。大括号内的变量名称必须与导入模块的外部接口名称相同(profile.js
)。
如果要重命名输入变量,import
命令必须使用关键字 as
来重命名输入变量。
import { lastName as surname } from './profile.js';
命令import
指定的所有变量都是只读的,因为它们的本质是输入接口。换句话说,不得在加载模块的脚本中覆盖该接口。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
上面的代码中,脚本加载了变量a
,如果重新赋值就会报错,因为a
是只读接口。但是,如果 a
是一个对象,则允许覆盖 a
的属性。
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
上述代码中,属性a
可以成功覆盖,其他模块也可以读取覆盖后的值。然而,这种表示方法很难检查错误。建议将所有输入变量视为完全只读,并且不要轻易更改其属性。
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果没有路径,只有模块的名称,那么必须有一个配置文件告诉 JavaScript 引擎模块的位置。
import { myMethod } from 'util';
在上面的代码中,util
是模块文件的名称。由于它不包含路径,因此必须将其配置为告诉引擎如何获取该模块。
注意,命令import
具有提升作用,会被提升到整个模块的头部并首先执行。
foo();
import { foo } from 'my_module';
上面的代码不会报错,因为import
是在调用foo
之前运行的。这种行为的本质是命令import
在编译阶段、代码执行之前执行。
集成模块加载(*)
// main.js
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));
上面的write方法一一指定了要加载的方法。总加载的写入方法如下。
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
导出默认
命令export default
指定模块的默认输出。这是唯一的价值。一个模块只能有一个默认输出。不建议在开发过程中使用。
// 第一组
export default function crc32() { // 输出
// ...
}
import crc32 from 'crc32'; // 输入
// 第二组
export function crc32() { // 输出
// ...
};
import {crc32} from 'crc32'; // 输入
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。