04ES 模块化
文件一多,全局变量就会互相踩脚。ES Module(import / export)
是浏览器原生的「分文件、按依赖组装」方案:
每个文件自带作用域,只暴露你写 export 的东西。
这也是后面学 Vite、React、Vue 的共同底座。
一、为什么需要模块
以前全局脚本
// a.js var name = 'A'; // b.js var name = 'B'; // 覆盖了 A! // 引入顺序错了就报错 <script src="a.js"></script> <script src="b.js"></script>
现在ES Module
// a.js
export const name = 'A';
// b.js
import { name } from './a.js';
console.log(name); // 'A',不会被覆盖
// main.js
<script type="module" src="main.js"></script>
二、导出 export:具名 vs 默认
| 导出 | 导入 | 说明 |
|---|---|---|
export const a = 1 |
import { a } from './m.js' |
具名导出,可导多个 |
export default fn |
import fn from './m.js' |
默认导出,每文件最多一个 |
export { a, b } |
import { a } from './m.js' |
打包导出,可 as 重命名 |
export * from './x.js' |
透传再导出 | 做「入口文件」常用 |
点按钮
三、模块的执行特性(面试常问)
| 特性 | 普通 script | type="module" |
|---|---|---|
| 作用域 | 共享全局 | 文件私有 |
| 严格模式 | 默认否 | 永远是 |
| 加载 | 阻塞 / defer / async | 默认类似 defer,按序执行 |
| 顶层 await | 不允许 | 允许(现代浏览器) |
| 重复加载 | 执行多次 | 只执行一次(有缓存) |
| import 位置 | — | 必须在模块顶层(不能在 if 里静态 import) |
import 会被提升到模块顶部;
条件加载请用动态 import()(返回 Promise)。四、file:// 下模块会挂?
用双击打开 HTML 时,浏览器常因 CORS 拒绝加载本地
.js 模块。这不是你代码写错,是安全策略。
type="module"。五、模块组织建议(小项目就够用)
六、常见问题 · 自问自答
为什么 import 写在函数 / if 里会报错?
静态 import 是编译期依赖,必须在模块顶层。
要按条件加载,用 const m = await import('./x.js'),
或把逻辑写在 if 里调用动态 import。
export default 和具名导出该怎么选?
现代实践优先具名导出:改名可搜索、跳转方便、利于 tree-shaking。 默认导出适合「本文件就一个主角」的场景(如 React 组件页)。 同一文件也可以两者都有,但团队规范最好统一。
为什么导入的变量改了值,源模块里也变了?
ESM 导入的是活绑定(live binding),不是值拷贝。
源文件里重新赋值,导入方能看见。要「复制一份」请自己 structuredClone 或写纯函数返回新对象。
报错 Failed to load module / CORS 怎么办?
① 是否用 file:// 双击打开 → 改本地服务器;
② 导入路径是否写了 ./ 或 ../(相对路径不能省);
③ 后缀是否是 .js(浏览器原生要求完整扩展名);
④ 是否把 type="module" 写对。
两个模块互相 import 会死循环吗?
ESM 能处理环依赖,但初始化顺序仍可能导致「先用后赋值」得到
undefined。尽量单向依赖;
必须共用类型/常量时,抽第三个「共享模块」。
七、本章小结
type="module"= 文件作用域 + 严格模式 + 按序延迟执行。- 导出:具名 / 默认;导入名字要对得上,可
as重命名。 - 静态 import 必须在顶层;按需加载用动态 import()。
- 本地开发请起 HTTP 服务器,别用 file:// 硬扛 CORS。
- 组织原则:一文件一职责,具名导出优先,依赖单向。
utils/format.js(格式化日期)在页面里 import 使用;
② 用动态 import 实现「点按钮才加载」一段动画逻辑;
③ 故意在 if 里写 import,把报错记进笔记。