作者最近开始了转全栈开发的旅程,正在写一个系列,记录每天学到的东西。这篇关于 ES6 模块 的文章就是系列的第一篇。
注意:如果还不熟悉变量、函数、类和对象这些 JS 概念,这篇内容可能不太好懂。
引言(Intro)
import和export两个关键字。- 模块化 = 每个文件都是它自己的模块;文件里定义的常量、变量、函数和类默认只属于该模块(私有),除非显式导出。
- 一个模块里导出的值,可以被显式导入它们的其他模块使用。
- 模块与普通脚本的区别:在普通脚本里,顶层声明的变量、函数和类会进入所有脚本共享的同一个全局上下文;而模块中,每个文件都有自己的私有上下文。
- ES6 模块内的代码自动处于严格模式(strict mode),不需要在文件顶部加
use strict指令。 - 严格模式修复了语言的一些缺陷,提供更强的错误检查,提高安全性。
- 严格模式下所有变量都必须声明。
ES6 导出(Exports)
可以从模块导出的符号有:常量、变量、函数、类。
export const PI = Math.PI;
export function degreesToRadians(d) { return d * PI / 180; }
export class Circle {
constructor(r) { this.r = r; }
area() { return PI * this.r * this.r; }
}
除了在每个实体前单独加 export 关键字,也可以在结尾统一列出:
export { Cirlce, degreesToRadians, PI };
这里的花括号并不表示对象的解构(destructuring)。
模块通常只导出一个值(典型是函数或类):
export default class BitSet {
// implementation omitted
}
导出分两种类型:
- 常规导出(regular exports)
- 默认导出(default exports)
用 export default 的默认导出可以导出任意表达式,包括匿名函数表达式和匿名类表达式。默认导出时如果出现花括号,表示的是对象字面量。
ES6 导入(Imports)
import BitSet from './bitset.js';
指定模块(与当前模块同目录的 bitset.js 文件)的默认导出,会成为当前模块中该标识符的值。
当模块把单个值作为默认导出时,导入它不需要花括号;如果是命名导出,就要用花括号。
在现代 JavaScript 环境里,模块说明符(module specifier)经常省略 .js 扩展名。
导入进来的值是一个 const 变量。
从外部包导入:
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
'react-router-dom' 是外部包,所以不需要相对路径。这告诉模块系统去 node_modules 里查找这个包。
从导出多个值的模块导入:
import { mean, stddev } from './stats.js';
对于没有名字的默认导出,导入时需要自己提供一个本地名字。
对于命名/常规导出,导入时沿用它们原来的名字。
从定义了很多导出的模块导入:
import * as stats from './stats.js';
这样的 import 语句会创建一个对象并赋给名为 stats 的常量。被导入模块的每个非默认导出都会成为这个 stats 对象的属性。
调用方式:stats.mean() 和 stats.stddev()。
引入一个没有导出的模块(即不导出任何值的模块):
import './analytics.js';
这个被引入的模块只会运行一次。
导入与导出的重命名(Imports and Exports with Renaming)
使用 as 关键字。
import { render as renderImage } from './imageutils.js';
import { render as renderUI } from './ui.js';
再导出(Re-Exports)
一个模块可以从其他模块导入符号,再把它们导出。
在 stats/stats.js 中:
import { mean } from './stats/mean.js';
import { stddev } from './stats/stddev.js';
export { mean, stddev };
上面这段可以合并成一条「再导出」语句:
export { mean } from './stats/mean.js';
export { stddev } from './stats/stddev.js';
假设我们想再导出 mean() 函数,同时用 average 作为它的别名:
export { mean, mean as average } from './stats/mean.js';
export { stddev } from './stats/stddev.js';
如果 mean.js 和 stddev.js 里的函数不是命名导出,而是默认导出:
export { default as mean } from './stats/mean.js';
export { default as stddev } from './stats/stddev.js';
把另一个模块的命名符号再导出为当前模块的默认导出:
export { mean as default } from './stats.js';
Web 上的 JS 模块
如果想在浏览器里原生使用 import 指令,必须用 <script type="module"> 标签告诉浏览器你的代码是一个模块。
<script type="module">import './main.js';</script>
这种内联的 JS 代码叫做内联模块(inline module)。
带 type="module" 属性的脚本,加载和执行方式与带 defer 属性的脚本一样:HTML 解析器一遇到 <script> 标签就开始加载代码。对模块来说,这个加载步骤可能是递归的,会加载多个 JS 文件。但代码执行要等 HTML 解析完成后才开始;一旦 HTML 解析完成,脚本(无论模块还是普通脚本)按它们在 HTML 文档中出现的顺序依次执行。
如果希望内联模块在代码加载完就立即执行,加上 async 属性——这样就不用等 HTML 解析完成。
普通 <script> 标签可以从互联网上的任何服务器加载 JS 文件;但 <script type="module"> 的代码只能从包含它的 HTML 文档同源(same origin)加载,或者在有正确的 CORS 头允许跨源加载时才能加载。
使用 import() 动态导入
静态导入:
import * as stats from './stats.js';
模块说明符 './stats.js' 不能用表达式代替——静态导入的说明符必须是字符串字面量。
动态导入:
import('./stats.js').then(stats => {
let average = stats.mean(data);
});
import('./stats.js') 返回一个 Promise 对象,代表加载并运行指定模块的异步过程。动态导入完成后,Promise 兑现(fulfilled),产生一个对象——就像前面静态导入中的 stats 变量。
或者,在 async 函数里:
async analyzeData(data) {
let stats = await import('./stats.js');
return {
average: stats.mean(data);
stddev: stats.stddev(data);
};
}
使用 import() 时,模块说明符可以是一个能求值为字符串的表达式。
import() 不是函数调用,而是一个运算符(operator)。
import.meta.url
import.meta 对象包含当前正在执行的模块的元数据。
import.meta.url 是模块被加载时的(绝对)URL。
例如:
function localStringsURL(locale) {
return new URL(`l10n/${locale}.json`, import.meta.url);
}
用 new 关键字实例化 URL 接口时会调用它的构造函数,构造函数接收 2 个参数:第一个是绝对 URL 或相对于基础 URL 的引用(这里就是这种情况),第二个是基础 URL。
小结
这篇笔记把 ES6 模块的核心概念过了一遍:export / import 语法、默认导出与命名导出的区别、重命名(as)、再导出、浏览器中的模块加载(type="module"、defer / async)、动态导入 import() 以及 import.meta.url——覆盖了日常开发中最常用的模块化写法。
💡 译者注:原文是作者的学习笔记,代码示例按原样保留(如
Cirlce的拼写、async analyzeData的写法、对象字面量里的分号),部分示例是语法演示而非可直接运行的完整代码。