讲解
早期 JavaScript 没有模块系统,所有脚本共享全局作用域,变量冲突、加载顺序依赖是家常便饭。ES6 模块(ESM)终结了这个时代:每个文件是一个独立模块,拥有自己的作用域;想要什么就从别的模块 import,想被别人用就 export。浏览器里用 启用,Node.js 里用 .mjs 后缀或 package.json 的 "type": "module"。
导出分两种:命名导出(export const PI = 3.14,一个模块可以有很多个,导入方用 import { PI } from './math.js',名字必须对应,可用 as 改名)和默认导出(export default ...,一个模块只能有一个,导入方自己起名字 import myMath from './math.js')。一个模块可以同时有两者。模块代码只执行一次,之后所有 import 拿到的是同一个模块实例(单例),导出的绑定还是「活的」——导出方改了值,导入方看到新值。
动态导入 import('./module.js') 返回 Promise,在需要时才加载模块,是实现按需加载、代码分割的基石。历史方案 CommonJS(require/module.exports)仍存在于大量 Node 老项目里,新代码一律用 ESM。注意模块文件必须通过 HTTP 服务访问(file:// 直开会被 CORS 拦截),本地练习时开个静态服务器。
示例
模块语法无法在本页面的运行沙盒中直接演示(import/export 只能在真正的模块文件里使用),看下面的文件示例:
// math.js —— 命名导出与默认导出
export const PI = 3.14159;
export function area(r) {
return PI * r * r;
}
export default function describe() {
return '数学工具模块';
}
// main.js —— 各种导入姿势
import describe from './math.js'; // 默认导出,名字自取
import { PI, area } from './math.js'; // 命名导出,名字对应
import { area as circleArea } from './math.js'; // as 改名
import * as math from './math.js'; // 全部收进命名空间对象
console.log(PI, area(2));
console.log(circleArea(2), math.PI, describe());
// 动态导入:用到时再加载
const heavy = await import('./heavy-feature.js');
heavy.run();
<!-- 浏览器启用模块 -->
<script type="module" src="./main.js"></script>
常见坑
- import 路径忘写扩展名:浏览器 ESM 和 Node ESM 都要求写全 './math.js',裸写 './math' 会 404(打包器里才可以省略)。
- 命名导出当默认导出导入:import area from './math.js' 去拿命名导出会报「没有默认导出」。花括号对应命名,无花括号对应默认。
- 用 file:// 打开模块页面:浏览器按 CORS 拒绝加载本地模块文件,控制台报跨域错。用任意静态服务器(如 npx serve)访问。
- 循环依赖:A import B、B import A 会让其中一方拿到「未初始化」的绑定,运行时报 TDZ 错误。出现循环依赖就该重新划分模块。
小结
ESM 用 export/import 组织代码,命名导出用花括号、默认导出不带;模块单例、绑定是活的;动态 import 支持按需加载。下一节学习数据交换格式 JSON。