| 涵盖:导入导出、执行机制、跨平台设计
一、模块基础:import 与 export
1. 导出(export)
| 类型 |
语法 |
特点 |
| 命名导出 |
export const name = 'Alice';
export function greet() {}
export { name, greet }; |
可多个,导入时名字必须匹配 |
| 默认导出 |
export default function() {}
export default obj; |
每模块最多一个,导入时可自定义名字 |
2. 导入(import)
| 场景 |
语法 |
说明 |
| 导入命名导出 |
import { name, greet } from './utils.js'; |
名字必须匹配 |
| 导入默认导出 |
import main from './utils.js'; |
名字可自定义 |
| 同时导入 |
import main, { version } from './utils.js'; |
默认 + 命名 |
| 全部导入 |
import * as utils from './utils.js'; |
utils 是命名空间对象 |
| 只执行(副作用) |
import './polyfill.js'; |
不导入值,只执行代码 |
二、模块的执行机制
✅ 模块只会执行一次(单例)
- 第一次
import:执行文件代码(顶层代码)。
- 后续
import:不执行,直接从模块缓存中取结果。
- 所有导入者共享同一个状态(如
let count = 0)。
1 2 3
| let count = 0; export const inc = () => count++;
|
🔗 import 的本质:创建“实时指针”(Live Binding)
import 不是“拷贝值”,而是创建一个指向目标变量的指针。
- 即使变量后续改变,所有导入者都能看到最新值。
1 2 3 4 5 6 7 8 9
| export let count = 0; export const inc = () => count++;
import { count } from "./counter.js"; console.log(count); inc(); console.log(count);
|
底层机制简述
当你写:
1
| import { greet } from "./hello.js";
|
浏览器或打包工具会:
- 解析模块路径:找到 hello.js
- 下载文件(如果是浏览器)
- 编译并执行模块(只执行一次)
- 建立模块记录(Module Record):记录导出了哪些绑定
- 链接导入:把 greet 映射到 hello.js 中的 greet 变量(引用)
- 执行代码
整个过程是模块化的、隔离的、可预测的
三、模块加载流程(4 阶段)
| 阶段 |
说明 |
| 1. 解析 |
静态分析 import/export,构建依赖图,验证语法 |
| 2. 加载 |
下载模块文件(网络/文件系统) |
| 3. 链接 |
创建“模块环境记录”,建立“实时绑定”(指针) |
| 4. 执行 |
执行顶层代码,初始化 export 变量 |
⚠️ 顺序:深度优先,依赖项先执行。
四、重新导出(Re-exporting)
| 语法 |
作用 |
是否包含 default |
export * from 'x.js' |
透传 x.js 的所有命名导出 |
❌ 否 |
export { name } from 'x.js' |
只导出 x.js 的 name |
❌ 否 |
export { default } from 'x.js' |
透传 default 导出 |
✅ 是 |
export { name as newName } from 'x.js' |
重命名导出 |
✅ 可重命名 |
🎯 用途:创建聚合模块(如 index.js)。
五、default vs 命名导出
| 问题 |
答案 |
export default 能用 {} 接收吗? |
❌ 不能!必须用 import xxx from |
命名导出能用 import xxx from 接收吗? |
❌ 不能!必须用 import { xxx } from |
| 如何同时导入? |
import defaultName, { named } from 'module' |
default 能重命名吗? |
✅ import { default as main } from 'module' |
六、模块的跨平台挑战与解决方案
❗ 问题:环境差异
| 环境 |
特有全局变量 |
| 浏览器 |
window, document, localStorage |
| Node.js |
process, require, __dirname |
❌ 模块若依赖 window,在 Node.js 中会报错。
✅ 解决方案 1:分层架构 —— “核心 + 绑定”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| +------------------+ | 应用层 | +--------+---------+ | +--------v---------+ | 绑定层 (Bindings)| | - browser.js | | - node.js | +--------+---------+ | +--------v---------+ | 核心层 (Core) | | - 纯逻辑:hash, validate | | - 无环境依赖 | +------------------+
|
✅ 优点
- 核心可无限复用。
- 绑定轻量、可替换。
- 易测试、易维护。
✅ 解决方案 2:运行时检测(Feature Detection)
1 2 3 4 5 6 7 8
| let password; if (typeof process !== "undefined" && process.env.PASSWORD) { password = process.env.PASSWORD; } else if (typeof document !== "undefined") { password = document.getElementById("password")?.value; } else { throw new Error("No password source"); }
|
🔑 原则:检测功能,不检测环境。
✅ 解决方案 3:Polyfill + 动态导入
1 2 3 4
| if (typeof fetch === "undefined") { globalThis.fetch = (await import("node-fetch")).default; }
|
🔑 使用 globalThis:在所有环境中都指向全局对象。
七、最佳实践清单
| 原则 |
建议 |
| 🧱 分离关注点 |
核心逻辑 vs I/O 操作 |
| 🔍 检测功能 |
typeof fetch === 'function' 而非 window |
🌐 使用 globalThis |
安全访问全局对象 |
| 📦 小模块,高内聚 |
一个模块只做一件事 |
| 🧪 测试核心 |
纯函数易单元测试 |
| 📚 文档说明 |
标注模块的环境依赖 |
🎯 总结口诀
“一缓存、二绑定、三核心、四检测”
- 一缓存:模块只执行一次,结果缓存共享。
- 二绑定:
import 是实时指针,不是值拷贝。
- 三核心:纯逻辑独立,不依赖环境。
- 四检测:用
typeof 检测功能,实现跨平台。