前言
在早期,JavaScript程序很小,用来你的 web 页面需要的地方提供一定交互,所以不需要多大的脚本。而后来,随着JavaScript程序越来越复杂,需要一种将 JavaScript 程序拆分为可按需导入的单独模块的机制,著名的 CommonJS
和 AMD
诞生了。前者主要用于服务端,后者则是用于浏览器。但这些都是社区提供的模块加载方案,随着ES6的到来,JavaScript原生模块(ES Module
)也正式在浏览器登场。
ES Module(简称ESM
)在所有现代浏览器都支持,它依赖于 import
和 export
命令。
export 命令
export
命令用于导出模块的功能
,import
命令用于导入其他模块提供的功能
。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export
关键字输出该变量。
最简单的写法是把export
放到要导出的项前面:
// user.js
export let name = 'Tom'
export let age = 10
export function jump() {
console.log('jump')
}
上面的代码向外部导出了两个变量和一个函数,我们还可以用另一种写法:
// user.js
let name = 'Tom'
let age = 10
function jump() {
console.log('jump');
}
export { name, age, jump }
将export写在最后,使用大括号指定要导出的功能。它和上一种方法是等价的,但是可以更直观的看到导出的内容。
在导出时,我们可以使用as
关键字将变量重命名
// user.js
let name = 'Tom'
let age = 10
function jump() {
console.log('jump');
}
export { name as n, age as a, jump as j }
此时外部就可以通过导入n
来使用name
的值。
import 命令
使用export导出模块后,就可以使用import导入这个模块。
// user.js
let name = 'Tom'
let age = 10
function jump() {
console.log('jump');
}
export { name, age, jump }
// main.js
import { name, age, jump } from './user.js'
console.log(name);
console.log(age);
jump()
// Tom
// 10
// jump
导入时也可以使用as
关键字重命名
// user.js
let name = 'Tom'
let age = 10
function jump() {
console.log('jump');
}
export { name, age, jump }
// main.js
import { name as n, age as a, jump as j } from './user.js'
console.log(n);
console.log(a);
j()
除了上面将变量逐个导入的方法之外,我们还可以导入整个模块,使用以下语法:
// main.js
// 这将获取user.js中所有可用的导出
import * as user from './user.js'
console.log(user.name);
console.log(user.age);
user.jump()
注意,import
命令输入的变量都是只读的,也就是说,不允许在加载模块的脚本里面,改写接口
// user.js
let name = 'Tom'
let obj = {}
export { name, obj }
// main.js
import { name, obj } from './user.js'
name = 'Jery' // 不合法 Uncaught TypeError: Assignment to constant variable
obj.prop = 'esm' // 合法
在HTML中导入模块
我们已经写好了简单的模块,接下来就是在html中引入main.js
这个文件。
<script src="./js/main.js"></script>
此时浏览器是会报错的,它会提示你语法错误。
这是因为浏览器不知道这个文件是模块,需要在script标签
添加type="module"
。
<script type="module" src="./js/user.js"></script>
此时浏览器已经可以正确识别ES6模块,就不会报错了。
ESM
也允许内嵌在网页中,语法行为与加载外部脚本完全一致
<script type="module">
import { name } from './js/user.js'
console.log(name); // Tom
</script>
默认导出 与 命名导出
上面讲的导出功能都是由 named exports
(命名导出) 组成 — 每个项目(无论是函数,常量等)在导出时都由其名称引用,并且该名称也用于在导入时引用它。
还有一种导出类型叫做 default export
(默认导出),用户可以快速上手使用,不需要了解模块有哪些属性和方法。
下面用代码来对比一下:
// 命名导出
export function jump() {
console.log('jump');
}
import { jump } from './user.js'
// 默认导出
export default function jump() {
console.log('jump');
}
import jump from './user.js'
可以看到,命名导出对应的import
语句需要使用大括号,默认导出是不需要使用大括号的。
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default
命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default
命令。
本质上,export default
相当于导出一个叫default
的变量,所以下面的代码是等价的
// user.js
function jump() {
console.log('jump');
}
export default jump
// 相当于
// export { jump as default }
// main.js
import jump from './user.js'
//相当于
// import { default as jump } from './user.js'
如果想在一条import
语句中,同时使用默认导入和命名导入,可以写成下面这样:
// user.js
let name = 'Tom'
let age = 10
function jump() {
console.log('jump');
}
export { name, age }
export default jump
// main.js
import jump, { name, age } from './user.js'
export 和 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起
// user.js
const name = 'Tom'
export { name }
// account.js
const username = 'Jack'
export { username }
// module.js
export { name } from './user.js'
export { username} from './account.js'
// 可以简单理解为
// import { name } from './user.js'
// export { name }
// import { username } from './account.js'
// export { username }
虽然export
和import
语句可以结合在一起,写成一行,但是name
和username
其实并没有被导入当前模块,只是相当于对外转发了这两个导出,在当前模块是不能使用的。
默认导出的复合写法如下:
export { default } from './user.js'
命名导出改为默认导出的复合写法如下:
export { name as default } from './user.js'
// 等同于
// import { name } from './user.js';
// export default name;
默认导出改为命名导出的复合写法如下:
export { default as name } from './user.js'
整个模块导出的复合写法如下:
export * as user from "./user.js"
// 等同于
// import * as user from "./user.js"
// export { user }
动态加载模块
import
和export
命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)。
// 报错
if (x === 1) {
import user from './user.js';
}
上面代码中,import
命令是在编译时处理,这时不会去分析或执行if语句,所以import语句放在if代码块之中毫无意义,因此会报语法错误。
ES2020提案
引入import()
函数,支持动态加载模块。import()
返回一个 Promise 对象,所以需要使用then()方法指定处理函数。
import('./user.js')
.then(module => {
// Do something with the module
})
.catch(err => {
// Error handling
})
为了代码的清晰,推荐使用await命令。
async function init() {
const user = await import('./user.js')
}