概述
ES6之前,JS没有模块系统,无法将一个大程序拆分为互相依赖的小文件
社区制定了一些模块加载方案最主要的是CommonJS和AMD,前者用于服务器,后者用于浏览器。
// CommonJS模块,运行时加载 let { stat, exists, readFile } = require('fs');
ES6模块的设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。而上述两者方案只有在运行时才能确定。
// ES6模块,编译时已确定,效率更好 import { stat, exists, readFile } from 'fs';
当然,这也导致了没法引用 ES6 模块本身,因为它不是对象
ES6模块采用严格模式
严格模式主要有以下限制。
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with
语句 - 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象,ES6模块中this指向undefined - 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
export命令
export
命令用于规定模块的对外接口
// profile.js
// 输出变量、函数或类 export let a = 'lpr' export let year = 2020
export function multiply(x, y) { return x * y; }
或者
// profile.js
let a = 111
function v1() { ... }
export { firstName, v1 }
通常情况下,export
输出的变量就是本来的名字,但是可以使用as
关键字重命名
function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion // 可以用不同的名字输出 }
export
命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系
export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
commonJS模块输出的是值得缓存,不存在动态更新
export
命令可以出现在模块的任何位置,只要处于模块顶层就可以
import命令
import
命令用于输入其他模块提供的功能
import { a,b,c } from './profile.js';
如果想为输入的变量重新取一个名字,import
命令要使用as
关键字,将输入的变量重命名。
import { lastName as surname } from './profile.js';
import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面改写接口。
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
import
命令具有提升效果,会提升到整个模块的头部,首先执行
import
是静态执行,所以不能使用表达式和变量
模块的整体加载
import { area, circumference } from './circle'; // 整体加载 import * as circle from './circle';
export default 命令
使用import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default
命令,为模块指定默认输出。
// export-default.js 默认输出一个匿名函数 export default function () { console.log('foo'); }
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字。
// import-default.js import customName from './export-default'; customName(); // 'foo'
比较一下默认输出和正常输出。
// 第一组 export default function crc32() { // 输出 // ... } import crc32 from 'crc32'; // 输入 // 第二组 export function crc32() { // 输出 // ... }; import {crc32} from 'crc32'; // 输入
export 与 import 的复合写法
export { foo, bar } from 'my_module'; // 可以简单理解为 import { foo, bar } from 'my_module'; export { foo, bar };
但需要注意的是,写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
模块可以继承
跨模块常量
如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。
// constants.js 模块 export const A = 1; export const B = 3; export const C = 4; // test1.js 模块 import * as constants from './constants'; console.log(constants.A); // 1 console.log(constants.B); // 3 // test2.js 模块 import {A, B} from './constants'; console.log(A); // 1 console.log(B); // 3
浏览器加载
传统方法
defer
与async
的区别是:defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺
浏览器加载 ES6 模块,也使用标签,但是要加入
type="module"
属性。
异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了标签的
defer
属性。
ES6 模块与 CommonJS 模块的差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
Node.js 加载
有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。从 v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
Node.js 要求 ES6 模块采用.mjs
后缀文件名。也就是说,只要脚本文件里面使用import
或者export
命令,那么就必须采用.mjs
后缀名。Node.js 遇到.mjs
文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"
。
如果不希望将后缀名改成.mjs
,可以在项目的package.json
文件中,指定type
字段为module
。