CommonJS
在 CommonJS 模块中,模块的输出是对象引用的拷贝。这意味着,如果导出的对象在模块内发生了修改,其他地方通过 require
导入的内容也会反映这些更改。
以下是详细说明:
-
模块缓存:
- CommonJS 模块在首次被
require
时,模块的代码会执行,导出对象会被创建并存储在缓存中。 - 后续对该模块的
require
调用将返回缓存中的导出对象。
- CommonJS 模块在首次被
-
引用行为:
- 如果模块导出的是一个对象或数组,外部对该导入对象的修改会影响其他地方导入的对象,因为它们引用的是同一个内存地址。
示例:
moduleA.js:
const data = { count: 1 };function increment() {data.count++;
}module.exports = { data, increment };
main.js:
const moduleA = require('./moduleA');
console.log(moduleA.data.count); // 输出: 1moduleA.increment();
console.log(moduleA.data.count); // 输出: 2const anotherImport = require('./moduleA');
console.log(anotherImport.data.count); // 输出: 2
解释:
module.exports
是一个对象的引用。require('./moduleA')
返回缓存中相同的引用。- 修改
moduleA.data
会反映在所有导入中。
注意:
- 如果直接导出原始值(如数字、字符串等),这将是一个值的拷贝,因为原始值是不可变的。例如:
moduleB.js:
let count = 1;function increment() {count++;
}module.exports = { count, increment };
main.js:
const moduleB = require('./moduleB');
console.log(moduleB.count); // 输出: 1moduleB.increment();
console.log(moduleB.count); // 输出: 1(值的拷贝,不会自动更新)
总结:
- CommonJS 模块导出的对象是引用的拷贝。
- 如果导出的是原始值,则导入后是值的拷贝,不会随着模块内的变化自动更新。
ES Module
在 ES Modules(ESM)中,模块的导出是通过引用的方式共享的。这意味着:
-
导出的值是动态绑定的:
- 导入的内容会实时反映模块中导出的最新状态(如果导出的值是对象或可变变量)。
- ES Modules 会确保模块导出和导入的值保持同步。
-
与 CommonJS 的区别:
- CommonJS 在模块加载时会将导出的内容生成一次并缓存(静态快照,原始值不会同步更新)。
- ESM 的导出是动态绑定的,即使导出的是原始值,更新模块内的值后,导入的地方也会反映出变化。
1. 对象引用
如果导出的是对象,导入的地方和模块内部共享同一个引用。
moduleA.mjs:
export const data = { count: 1 };export function increment() {data.count++;
}
main.mjs:
import { data, increment } from './moduleA.mjs';console.log(data.count); // 输出: 1increment();
console.log(data.count); // 输出: 2
解释:
data
是通过引用共享的,模块内部修改会直接反映到导入的地方。
2. 原始值(动态绑定)
如果导出的是原始值(如数字、字符串),模块内部更新的值会同步到导入的地方。
moduleB.mjs:
export let count = 1;export function increment() {count++;
}
main.mjs:
import { count, increment } from './moduleB.mjs';console.log(count); // 输出: 1increment();
console.log(count); // 输出: 2
解释:
count
是动态绑定的,尽管它是原始值,模块内部的更新会反映到导入的地方。
ESM 的特点:
-
静态结构:
- ES Modules 是在编译时解析的,
import
和export
必须位于顶层。 - 无法在运行时动态导入或导出(但可以使用
import()
动态加载模块)。
- ES Modules 是在编译时解析的,
-
实时绑定:
- ESM 的导出是动态绑定的,导入的值会随模块内的更新而更新。
-
单例模式:
- ES Modules 是单例的,多次导入同一个模块时,得到的都是同一个模块实例。
对比总结
特性 | CommonJS | ES Modules |
---|---|---|
导出机制 | 值的拷贝(原始值),引用拷贝(对象) | 动态绑定 |
导入时机 | 运行时(动态加载) | 编译时(静态加载) |
更新机制 | 静态快照 | 实时更新 |
模块缓存 | 是 | 是 |
用法 | require() | import/export |
因此,ESM 更适合处理动态绑定和模块间实时同步的场景。
其他区别
1. 模块加载时机
- CommonJS:模块加载是 同步 的,模块加载时会立即执行,并返回模块的导出内容。因此,
require
语句会阻塞,直到模块完全加载并执行完成。 - ESM:模块加载是 异步 的,在浏览器和 Node.js 中,ESM 模块在加载时不会阻塞,尤其是在浏览器环境下,模块加载会是异步的。
2. 文件扩展名
- CommonJS:支持
.js
,.json
, 和.node
扩展名的文件。模块导入时,如果没有扩展名,Node.js 会自动尝试加载.js
、.json
或.node
文件。 - ESM:在 Node.js 环境下,ESM 强制要求指定扩展名,除非使用
.js
文件且该文件是 ES Module(在package.json
中指定"type": "module"
)。这意味着必须明确指定扩展名,如import x from './module.mjs'
或import x from './module.js'
。
3. 模块的加载方式
- CommonJS:在执行
require
时,模块被加载并执行一次,之后会缓存模块的导出内容。模块执行的顺序是按调用require
的顺序同步执行。 - ESM:ESM 模块的加载是 按需 加载的,ESM 模块会被动态解析并可能会被异步加载。ESM 模块会被延迟执行,且加载过程支持静态分析,允许更强大的工具进行优化和死代码删除。
4. import
和 export
语法
-
CommonJS:
- 导入:
const foo = require('foo');
- 导出:
module.exports = foo;
或exports.foo = foo;
- 导入:
-
ESM:
- 导入:
import foo from 'foo';
或import { bar } from 'foo';
- 导出:
export default foo;
或export { foo };
ES Modules 使用静态的
import
和export
语法,在编译时就可以解析出模块的依赖关系,而 CommonJS 使用动态的require
和module.exports
,只能在运行时解析。 - 导入:
5. this
的行为
-
CommonJS:在模块中,
this
默认指向module.exports
。这意味着如果你没有显式地导出模块,this
将指向一个空对象({}
)。// CommonJS console.log(this); // 输出: {} this.foo = 'bar'; console.log(module.exports); // 输出: { foo: 'bar' }
-
ESM:
this
在 ESM 模块中并不会指向exports
或module.exports
,它指向undefined
。在 ESM 中,export
和import
被静态解析,this
并不是用于模块导出的机制。// ESM console.log(this); // 输出: undefined
6. 异步性
-
CommonJS:模块加载是同步的,因此适合于服务器端的环境,尤其是在 Node.js 中,文件读取通常是同步的。
-
ESM:ESM 支持异步加载模块,尤其是在浏览器中,ESM 是按需加载的,可以延迟模块的加载,提高性能。对于 Node.js,也提供了
import()
异步加载的能力。// 使用动态导入 import('./module.js').then(module => {console.log(module); });
7. 循环依赖处理
-
CommonJS:处理循环依赖时,CommonJS 会在模块第一次加载时返回模块的当前状态,后续
require
调用会返回这个已经部分执行的模块。这意味着如果一个模块还未完全执行,其他模块就可以访问到它的部分导出。 -
ESM:ESM 通过引入 动态绑定 的方式来处理循环依赖。如果模块 A 导入模块 B,而模块 B 又导入模块 A,模块 A 和 B 中的导出会是动态的绑定关系,始终反映最新的值。
8. 命名空间导出
-
CommonJS:导出的内容是一个对象,可以直接添加到
module.exports
或exports
上,这通常意味着你可以在导出时使用任意结构(如对象、数组、函数等)。// CommonJS 导出 module.exports = { foo: 'bar', baz: 'qux' };
-
ESM:ESM 使用静态导出和导入的机制,且导入时会自动生成一个“命名空间”对象。这意味着你可以选择性地导入模块的部分功能。
// ESM 导出 export const foo = 'bar'; export const baz = 'qux';
9. require
和 import
的行为
-
CommonJS:
require
是一个动态函数,可以在任何地方使用,因此可以在条件语句、循环中动态加载模块。if (condition) {const module = require('./module'); }
-
ESM:
import
是静态的,必须在模块的顶部使用,并且不能在条件语句、循环中动态加载。import
的静态特性使得打包工具(如 Webpack)能够优化模块的加载顺序和去除死代码。// ESM 只能在顶部使用 import { foo } from './module';
10. 浏览器支持
-
CommonJS:原生不支持浏览器,通常需要通过打包工具(如 Webpack、Browserify)进行转换。
-
ESM:ESM 从一开始就设计为浏览器支持的标准。现代浏览器原生支持
<script type="module">
标签,可以直接加载 ESM 模块。Node.js 在 v12+ 也原生支持 ESM。
11. 模块作用域
-
CommonJS:模块是封装在一个函数中,模块内部的变量和函数默认不污染全局作用域。
-
ESM:ESM 的模块作用域也类似,每个模块都有自己的作用域,且不污染全局作用域。ESM 具有更强的静态分析能力和模块系统支持。
总结
特性 | CommonJS | ES Modules |
---|---|---|
加载方式 | 同步加载 | 异步加载 |
语法 | require() , module.exports | import , export |
模块缓存 | 模块首次加载时缓存 | 模块首次加载时缓存 |
模块作用域 | 模块内有自己的作用域,this 默认指向 module.exports | 模块内有自己的作用域,this 指向 undefined |
循环依赖 | 部分执行的模块对象 | 动态绑定,解决循环依赖 |
扩展名 | 支持 .js , .json , .node | 强制需要扩展名(如 .js , .mjs ) |
适用场景 | 主要用于服务器端,Node.js | 适用于浏览器和 Node.js,支持异步加载 |