CommonJS 和 ES Modules 的区别是什么?

核心区别

特性 CommonJS (CJS) ES Modules (ESM)
语法 require/module.exports import/export
加载方式 运行时动态加载 编译时静态分析
输出 值的拷贝 值的引用
this this === module.exports this === undefined
循环依赖 返回已执行部分 引用绑定
顶层 this module.exports undefined
动态导入 require() import()
适用 Node.js 服务端 浏览器 + Node.js

值拷贝 vs 值引用

CommonJS — 值拷贝:

// lib.js
let count = 0
function increment() { count++ }
module.exports = { count, increment }

// app.js
const { count, increment } = require('./lib')
increment()
console.log(count) // 0(拷贝,不受影响)

ESM — 值引用:

// lib.mjs
export let count = 0
export function increment() { count++ }

// app.mjs
import { count, increment } from './lib.mjs'
increment()
console.log(count) // 1(引用,同步变化)

动态加载

// CJS:require 是同步的
const lib = condition ? require('./a') : require('./b')

// ESM:import 是静态的,但可以用 import() 动态加载
const lib = await import(condition ? './a.mjs' : './b.mjs')

导出方式

// CJS
module.exports = { add, sub }     // 对象导出
module.exports.add = add            // 单个导出
module.exports = function() {}      // 函数导出

// ESM
export { add, sub }                 // 命名导出
export default function() {}         // 默认导出
export const PI = 3.14              // 内联导出

导入方式

// CJS
const { add } = require('./math')   // 解构导入
const math = require('./math')       // 整体导入

// ESM
import { add } from './math.mjs'     // 命名导入
import math from './math.mjs'        // 默认导入
import * as math from './math.mjs'   // 命名空间导入
import './polyfill.mjs'              // 副作用导入

Node.js 中的模块识别

// package.json
{
  "type": "module"   // .js 文件作为 ESM 处理
  // 或 "type": "commonjs"  // 默认
}

// 文件扩展名
.js   → 取决于 package.json type
.mjs  → 始终 ESM
.cjs  → 始终 CommonJS

互操作

// ESM 中导入 CJS
import pkg from 'commonjs-package'  // pkg === module.exports

// CJS 中导入 ESM(异步)
const esm = await import('./esm-module.mjs')

Tree Shaking

最佳实践

同分类其他题目