Skip to content

模块化

作者:青见春山
发表于:2026-07-29
字数统计:6000 字
预计阅读21分钟

涵盖 CommonJS 与 ES6 Module 在加载时机、值拷贝/引用、语法、循环依赖、Tree Shaking 等方面的差异。

一、核心差异速览表

维度CommonJSES6 Module
设计目标服务端(Node.js)通用(浏览器 + 服务端)
加载时机运行时加载编译时加载(静态分析)
加载方式同步加载异步加载(浏览器环境)
导出本质值拷贝值引用(实时绑定)
语法require() / module.exportsimport / 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)        // 18

3. 静态结构的限制

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 的核心区别是什么?

:主要有三点:

  1. 加载时机:CommonJS 运行时加载,ES6 Module 编译时加载
  2. 导出方式:CommonJS 值拷贝,ES6 Module 值引用
  3. 语法: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 两者都在
循环依赖要注意,面试常考这两个

八、关联文档