随着 JavaScript 向现代化发展,模块化编程成为大型项目的基石。ES6 引入了原生模块系统(ES Modules, ESM),为开发者提供了更加高效、规范和可优化的模块管理方式。
本文将系统讲解 ES6 模块的核心机制,并重点介绍 import.meta
以及模块对象的结构和用法,帮助你全面理解 ESM 在实践中的应用。
一、ES6 模块核心特性
1. 静态结构(Static Structure)
ES6 模块在 编译阶段 即确定模块依赖,便于构建工具进行优化(如 Tree-shaking)。
js
import { sum } from './math.js';
相比之下,CommonJS 使用 require()
是运行时动态加载的,不利于静态分析。
2. 模块作用域隔离
每个模块都有自己的作用域,定义的变量不会污染全局,也不会影响其他模块。
3. 导出方式:命名导出与默认导出
js
// math.js
export const PI = 3.14;
export default function (x) {
return x * PI;
}
js
// main.js
import circle, { PI } from './math.js';
二、模块加载机制简析
浏览器中:
- 通过
<script type="module">
加载; - 模块脚本默认严格模式;
- 同源策略更严格(默认启用 CORS);
- 模块异步加载,不阻塞主线程;
- 每个模块只会被加载和执行一次(即使被多次引用)。
Node.js 中:
- 启用
.mjs
后缀,或设置package.json
中"type": "module"
; - 使用文件路径作为模块标识;
- 默认禁用 CommonJS 的全局变量(如
__dirname
),推荐使用import.meta.url
。
三、模块对象结构解析
当你使用 import * as mod
导入模块时,得到的是一个模块对象,它包含了该模块导出的所有绑定。
js
// example.js
export const version = '1.0.0';
export function greet(name) {
return `Hello, ${name}`;
}
export default 'default-export';
js
// main.js
import * as mod from './example.js';
console.log(Object.keys(mod)); // ['version', 'greet', 'default']
模块对象的特点:
特性 | 说明 |
---|---|
属性绑定是实时的 | 称为 Live Binding,导入的是"引用"而非"值拷贝" |
对象不可扩展 | Object.isFrozen(mod) === true |
包含 default 属性 | 如果有默认导出,则可通过 mod.default 访问 |
示例:live binding 的效果
js
// counter.js
export let count = 0;
export function inc() {
count++;
}
js
// main.js
import * as counter from './counter.js';
console.log(counter.count); // 0
counter.inc();
console.log(counter.count); // 1(绑定生效)
四、如何遍历模块对象
你可以使用 Object.keys
或 Object.entries
遍历模块对象的所有导出成员:
js
import * as mod from './example.js';
for (const key of Object.keys(mod)) {
console.log(`${key}:`, mod[key]);
}
或:
js
Object.entries(mod).forEach(([key, value]) => {
console.log(`${key}:`, value);
});
输出:
vbnet
version: 1.0.0
greet: [Function: greet]
default: default-export
五、import.meta
:模块元信息对象
import.meta
是什么?
它是一个由运行时自动填充的模块元信息对象,包含当前模块的上下文信息。
js
console.log(import.meta.url); // 模块的绝对 URL
输出示例:
perl
file:///Users/mlight/project/main.js
浏览器与 Node.js 中的差异:
属性 | 浏览器支持 | Node.js 支持 | 说明 |
---|---|---|---|
import.meta.url |
✅ | ✅ | 模块绝对路径(file:// 格式) |
import.meta.env |
🔶 | ❌(除构建工具注入) | 构建工具(如 Vite)注入环境变量 |
js
// Vite 自动注入
if (import.meta.env.DEV) {
console.log('开发模式');
}
与模块对象的区别:
对象 | 来源 | 内容说明 |
---|---|---|
模块对象 | import * as mod |
包含导出成员的引用 |
import.meta |
特殊关键字 | 提供当前模块的元信息,如 URL、环境变量等 |
六、动态导入与顶层 await
import()
:动态导入模块
js
import('./math.js').then(mod => {
console.log(mod.sum(2, 3));
});
特点:
- 返回 Promise;
- 可用于懒加载、按需加载、路由分包;
- 可用于条件导入模块。
顶层 await
在模块中允许在顶层使用 await
(无需函数封装):
js
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
⚠️ 仅适用于 ESM 模块,不支持普通 <script>
脚本。
七、模块循环引用(Circular Import)
ES6 模块支持循环引用,但不建议过度依赖。
js
// a.js
import { b } from './b.js';
export const a = 'A';
console.log('from b:', b);
// b.js
import { a } from './a.js';
export const b = 'B';
console.log('from a:', a);
循环引用的变量可能为 undefined
,ESM 会保证模块执行顺序正确,但需谨慎使用。
八、综合实践示例
js
// math.js
export const PI = 3.14;
export const add = (a, b) => a + b;
export default 'Math Module';
js
// main.js
import * as math from './math.js';
console.log('模块对象属性:');
for (const [k, v] of Object.entries(math)) {
console.log(` ${k}:`, v);
}
console.log('\nimport.meta 信息:');
console.log(` 当前模块 URL: ${import.meta.url}`);
九、总结
项目 | 说明 |
---|---|
模块对象 | import * as mod 获取到的对象,包含所有导出成员 |
import.meta |
模块级元信息,如 URL、环境变量等 |
Live Binding | 导入的是绑定引用,值是"活的",会跟随原模块更新 |
默认导出 | 对应 mod.default 属性 |
动态导入与懒加载 | 使用 import() 动态加载模块 |
顶层 await |
在模块最外层使用 await ,更灵活地处理异步逻辑 |