1.概述
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
Module的好处:
- 不再需要
UMD
模块格式了,将来服务器和浏览器都会支持ES6
模块格式。目前,通过各种工具库,其实已经做到了这一点。 - 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者
navigator
对象的属性。 - 不再需要对象作为命名空间(比如
Math
对象),未来这些功能可以通过模块提供。
2.严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"
;。
3.export命令
模块功能主要由两个命令构成:
export
命令:用于规定模块的对外接口import
命令:用于输入其他模块提供的功能。
一个模块就是一个独立的文件。
该文件内部的所有变量,外部无法获取。
如果你希望外部能够读取模块内部的某个变量,就必须使用export
关键字输出该变量。
下面是一个 JS 文件,里面使用export
命令输出变量。
// 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
命令还可以输出函数或类(class)。
export function multiply(x, y) {
return x * y;
};
export
命令中,可以使用as
关键字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
上面代码使用as
关键字,重命名了函数v1
和v2
的对外接口。重命名后,v2
可以用不同的名字输出两次。换句话说,就是使用as
关键字重命名不会覆盖之前使用as
关键字命名的元素,而是会让元素有两个不同的名字。
注意:
export
命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。export
命令是向外抛的值的 引用 ,所以说不能省略大括号,否则会认为你是抛出的值,而不是引用而报错export
语句输出的接口,与其对应的值是 动态绑定 关系,即通过该接口,可以取到模块内部实时的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代码输出变量foo
,值为bar
,500 毫秒之后值变成baz
。
export
命令,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。import
命令也是如此
这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
4.improt命令
使用
export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过import
命令加载这个模块。
使用import
时是引用export
抛出的接口。注意是引用
// main.js
import { firstName, lastName, year } from './profile.js';
//使用 import 命令加载 profile.js文件
function setName(element) {
element.textContent = firstName + ' ' + lastName;// 引入后可直接当做变量使用
}
import
命令接受一对大括号,里面指定要从其他模块导入的变量名。
大括号里面的变量名,必须与被导入模块(profile.js
)对外接口的名称相同。
- 同样,
import
可以使用as
关键字命名
import { lastName as surname } from './profile.js';
import
命令具有提升效果,会提升到整个模块的头部,首先执行。(import
命令在编译阶段执行)import
命令不允许在加载模块的脚本里面,改写接口。因为import
输入的变量都是只读的,它的本质是输入接口。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
上面代码中,脚本加载了变量a,对其重新赋值就会报错。因为a
是一个只读的接口。
但是,如果a
是一个对象,改写a
的属性是允许的。
import {a} from './xxx.js'//a 为一个对象
a.foo = 'hello'; // 合法操作
上面代码中,a
的属性可以成功改写,并且其他模块也可以读到改写后的值。
建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。因为这种写法很难查错。
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
import {myMethod} from 'util';
上面代码中,util
是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块。
- 由于
import
是静态执行,所以命令中不能使用表达式和变量 - 最后,
import
语句会执行所加载的模块,因此可以有下面的写法。
import 'lodash'; //仅仅执行 lodash 模块,但是不输入任何值
import 'lodash'; //重复执行同样的模块,也只会执行一次
import { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module'; // 对应的是同一个my_module实例。
5.模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(*
)指定一个对象,所有输出值(通过export
抛出的值)都加载在这个对象上面。
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
//因为所有输出值都加载在这个用 * 号指定对象上面,所以可以通过对象的.(点)方法调用
上面的 area
和 circumference
是模块输出的方法,使用整体加载的方法,使circle
对象接收
6.export default 命令
export default
命令,可以为模块指定默认输出。
从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令
语法:
// export-default.js
export default function () {
console.log('foo');
}
import-default.js
import customName from './export-default'; //指定名称
customName(); // 'foo'
export-default.js
是一个模块文件,使用export default
默认输出一个函数
其他的模块加载该模块时,import
还可以为它指定任意名字(不用带大括号),因为export default
在模块中只能有一个。
export default
命令用在非匿名函数前,也是可以的。export default
也可以用来输出类。
export default class { ... }
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';
因此,它后面不能跟变量声明语句。
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1; //语法会理解为 export {var a = 1 as default},导致报错
又因为export default
命令的本质是将后面的值,赋给default
变量,所以可以直接将一个值写在export default
之后。
// 正确
export default 42; //指定了对外的接口,为 default
// 报错
export 42;//未指定对外的接口
export 与 export default 的区别
export
与export default
均可用于导出常量、函数、文件、模块、类等- 在一个文件或模块中,
export
、import
可以有多个,export default
仅有一个 - 使用
export default
导出,再导入时不需要 { }
7.export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import
语句可以与export
语句写在一起。
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
这样写相当于转发了这个接口,导致当前的模块不能使用foo
和bar
。
//默认接口的写法
export { default } from 'foo';
//具名接口改为默认接口的写法
export { es6 as default } from './someModule';
//默认接口改名为具名接口
export { default as es6 } from './someModule';
// 整体输出
export * from 'my_module';
上面是各个方式的写法
8.模块的继承
模块之间可以继承。
// circleplus.js
export * from 'circle'; // 表示再输出 circle 中的所有属性和方法,不包括 default 方法,因为export *命令会忽略 default 方法
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
这时,也可以将circle
的属性或方法,改名后再输出。
// circleplus.js
export { area as circleArea } from 'circle';
//可简单理解为
import {area} from 'circle'
export {area as circleArea};
加载上面模块的写法如下。
// main.js
import * as math from 'circleplus'; //将模块输出的整体改名为 math
import exp from 'circleplus'; //此处 exp 表示,将 circleplus 模块的默认方法(default)加载为 exp 方法
console.log(exp(math.e));
9.跨模块常量
//constants.js
export const A = 1;
export const B = 3;
export const C = 4;
//text1.js
import * as constants from './constants'
console.log(constants.A); // 1
console.log(constants.B); // 3
//text2.js
imports {A,B} from './constants'
console.log(A); // 1
console.log(B); // 3
如果要使用的常量非常多,可以建一个专门的constants
目录,将各种常量写在不同的文件里面,保存在该目录下。
// constants/db.js
export const db = {
url: 'http://my.couchdbserver.local:5984',
admin_username: 'admin',
admin_password: 'admin password'
};
// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
然后,将这些文件输出的常量,合并在index.js
里面。使用的时候,直接加载index.js
就可以了。
// constants/index.js
export {db} from './db';
export {users} from './users';
// script.js
import {db, users} from './constants/index';
10.import()
import()函数是一个提案,可实现动态加载
11.浏览器的加载方式
HTML 网页中,浏览器通过
<script>
标签加载 JavaScript 脚本。 默认情况下,JavaScript
脚本为同步加载,遇到脚本时,等到执行完脚本,再继续向下渲染。
如果是外部脚本,还必须加入脚本下载的时间。
浏览器允许脚本异步加载,下面就是两种异步加载的语法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
上面代码中,<script>
标签打开defer
或async
属性,脚本就会异步加载。
渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
defer
与async
的区别:
defer
:
- 等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行
- 若有多个
defer
脚本,会按照它们在页面出现的顺序加载。
async
:
async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。- 若有多个
async
脚本,不能保证它的加载顺序。
简单来说,
defer
是“渲染完再执行”(下载完毕,等待渲染),async
是“下载完就执行”。
12.ES6模块加载规则
浏览器加载 ES6 模块,也使用
<script>
标签,但是要加入type="module"
属性。让浏览器知道这是一个 ES6 模块。
带有type="module"
的<script>
,都是异步加载,等同于打开了<script>
标签的defer
属性。
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
也可以自己输入 async
属性,加载后就可以执行(会中断渲染,执行完成再恢复渲染)
<script type="module" src="./foo.js" async></script>
ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。
<script type="module">
import utils from "./utils.js";
// other code
</script>
对于外部的模块脚本(上例是foo.js),有几点需要注意:
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明
use strict
。 - 模块之中,可以使用
import
命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export
命令输出对外接口。 - 模块之中,顶层的
this
关键字返回undefined
,而不是指向window
。 - 同一个模块如果加载多次,将只执行一次。