模块化
发表于:2026-07-29
字数统计:6000 字
预计阅读21分钟
涵盖 CommonJS 与 ES6 Module 在加载时机、值拷贝/引用、语法、循环依赖、Tree Shaking 等方面的差异。
一、核心差异速览表
| 维度 | CommonJS | ES6 Module |
|---|---|---|
| 设计目标 | 服务端(Node.js) | 通用(浏览器 + 服务端) |
| 加载时机 | 运行时加载 | 编译时加载(静态分析) |
| 加载方式 | 同步加载 | 异步加载(浏览器环境) |
| 导出本质 | 值拷贝 | 值引用(实时绑定) |
| 语法 | require() / module.exports | import / export |
| 缓存机制 | 首次加载后缓存 | 模块只执行一次,但导出值动态绑定 |
| this 指向 | 当前模块 | undefined |
| Tree Shaking | ❌ 不支持 | ✅ 支持(静态分析) |
| 循环依赖 | 返回已执行部分(可能不完整) | 编译时确定,但需注意执行顺序 |
二、加载机制的差异
1. 运行时 vs 编译时
JavaScript
// CommonJS - 运行时加载
if (condition) {
const module = require('./module') // ✅ 可以动态加载
}
// ES6 Module - 编译时加载
if (condition) {
import { fn } from './module' // ❌ 语法错误,import 必须在顶层
}
// ES6 动态导入 - 运行时
if (condition) {
import('./module').then(module => {
module.fn()
})
}2. 值的拷贝 vs 值的引用
JavaScript
// CommonJS - counter.js
let count = 0
module.exports = {
count,
increment: () => count++
}
// CommonJS - main.js
const { count, increment } = require('./counter')
console.log(count) // 0
increment()
console.log(count) // 0(值没变,是拷贝)JavaScript
// ES6 Module - counter.js
export let count = 0
export const increment = () => count++
// ES6 Module - main.js
import { count, increment } from './counter'
console.log(count) // 0
increment()
console.log(count) // 1(值变了,是引用)三、语法详解
CommonJS 语法
导出方式
JavaScript
// 方式1:直接赋值给 exports
exports.name = 'king'
exports.age = 18
// 方式2:赋值给 module.exports(推荐)
module.exports = {
name: 'king',
age: 18
}
// 方式3:混合使用(注意 exports 是 module.exports 的引用)
exports.name = 'king'
module.exports.age = 18 // ✅ 正确
// exports = { name: 'king' } // ❌ 错误,断开引用导入方式
JavaScript
// 导入自定义模块
const myModule = require('./myModule')
// 导入核心模块
const fs = require('fs')
// 导入第三方模块
const lodash = require('lodash')
// 解构导入
const { name, age } = require('./user')ES6 Module 语法
导出方式
JavaScript
// 方式1:单独导出
export const name = 'king'
export function sayHi() {
console.log('hi')
}
// 方式2:统一导出
const name = 'king'
const age = 18
export { name, age }
// 方式3:重命名导出
const name = 'king'
export { name as userName }
// 方式4:默认导出(每个模块只能有一个)
export default function() {
console.log('default')
}
// 或
export default { name, age }
// 方式5:聚合导出
export * from './module'
export { name, age } from './module'导入方式
JavaScript
// 导入命名导出
import { name, age } from './user'
// 导入重命名
import { name as userName } from './user'
// 导入默认导出
import defaultFn from './module'
// 混合导入
import defaultFn, { name, age } from './module'
// 全部导入
import * as user from './user'
user.name // 注意:不会包含 default
// 动态导入(返回 Promise)
import('./module').then(module => {
module.default()
})四、常见陷阱与注意事项
CommonJS 陷阱
1. exports 重新赋值失效
JavaScript
// ❌ 错误用法
exports = { name: 'king' } // 断开引用,外部无法获取
// ✅ 正确用法
module.exports = { name: 'king' }
// 或
exports.name = 'king'2. 循环依赖问题
JavaScript
// a.js
exports.a = 1
const b = require('./b')
console.log(b.b) // 可能为 undefined
// b.js
exports.b = 2
const a = require('./a')
console.log(a.a) // 1(部分加载)ES6 Module 陷阱
1. import 提升
JavaScript
// import 会提升,但行为与 var 不同
console.log(name) // 实际会报 ReferenceError
import { name } from './module'2. export default 与 export 混用
JavaScript
// module.js
export default { name: 'king' }
export const age = 18
// main.js
import user, { age } from './module'
console.log(user.name) // king
console.log(age) // 183. 静态结构的限制
JavaScript
// ❌ 错误:不能动态导入
const moduleName = './module'
import { fn } from moduleName
// ❌ 错误:不能在条件语句中
if (true) {
import { fn } from './module'
}五、实际开发场景
Node.js 环境
JavaScript
// 方式1:纯 CommonJS(传统 Node 项目)
// package.json 中无 "type": "module"
const fs = require('fs')
module.exports = { readFile: fs.readFile }
// 方式2:在 Node 中使用 ES6 Module
// package.json 中设置 "type": "module"
// 或使用 .mjs 扩展名
import fs from 'fs'
export const readFile = fs.readFile浏览器环境
HTML
<!-- 方式1:传统方式(需要打包工具) -->
<script src="bundle.js"></script>
<!-- 方式2:原生 ES Module -->
<script type="module">
import { fn } from './module.js'
fn()
</script>
<!-- 方式3:动态导入 -->
<script type="module">
import('./module.js').then(module => {
module.fn()
})
</script>混合使用(谨慎)
JavaScript
// ES6 Module 导入 CommonJS(Node 环境)
import pkg from './commonjs-module' // pkg 是整个 module.exports
// CommonJS 导入 ES6 Module(需要动态 import)
const { default: esModule } = await import('./es-module.mjs')六、面试高频问答
Q1: CommonJS 和 ES6 Module 的核心区别是什么?
答:主要有三点:
- 加载时机:CommonJS 运行时加载,ES6 Module 编译时加载
- 导出方式:CommonJS 值拷贝,ES6 Module 值引用
- 语法:CommonJS 动态
require(),ES6 Module 静态import/export
Q2: 为什么 CommonJS 不适合浏览器?
答:CommonJS 是同步加载,在服务端文件在本地硬盘,影响不大。但在浏览器环境,模块需要从网络下载,同步加载会阻塞页面渲染。所以浏览器需要异步加载方案(如 ES6 Module 的 <script type="module">)。
Q3: export default 和 export 有什么区别?
答:
export default每个模块只能有一个,导入时不需要{},可以任意命名export可以有多个,导入时必须用{},名称必须匹配export default导出的是值,export导出的是接口
Q4: 什么是 Tree Shaking?为什么 ES6 Module 支持?
答:Tree Shaking 是移除未使用代码的优化技术。ES6 Module 的 import/export 是静态的,在编译时就能确定依赖关系,所以打包工具可以分析出哪些导出没有被使用,从而移除它们。CommonJS 的 require 是动态的,无法在编译时确定,所以不支持 Tree Shaking。
七、快速记忆口诀
Plain
CommonJS 运行时,值拷贝同步加载
module.exports 是正解,exports 是引用别乱改
ES6 模块编译时,值引用异步加载
export default 只能一,import 静态别乱摆
浏览器要用 ES6,Node 两者都在
循环依赖要注意,面试常考这两个八、关联文档
- 构建工具(ESM 原理的三阶段)