CommonJS与ES6模块机制深度解析:加载、缓存与循环引用差异
本文深入对比CommonJS与ES6模块的核心差异。CommonJS采用值拷贝与运行时缓存机制,循环加载时仅输出已执行部分;ES6模块则基于动态只读引用,支持实时值同步且循环加载更为灵活。文章通过具体代码示例解析基本类型与复杂类型的处理逻辑,帮助开发者理解不同模块系统的执行行为。
引言与背景
经过三个月的实习期后,近期在面试中遇到了关于模块循环加载的考题。借此机会,本文将对CommonJS与ES6模块之间的重要区别进行总结,重点探讨引用机制而非语法层面的差异。
CommonJS模块机制详解
CommonJS模块机制
针对基本数据类型,CommonJS采用复制机制,即模块输出会被缓存。此时,其他模块可以对该模块输出的变量重新赋值。
针对复杂数据类型,CommonJS采用浅拷贝机制。由于两个模块引用的对象指向同一内存空间,因此对一个模块的值进行修改时,会影响另一个模块。
使用require命令加载模块时,会立即执行该模块的全部代码。
使用require命令加载同一模块时,不会再次执行,而是直接返回缓存中的值。这意味着CommonJS模块无论加载多少次,仅在第一次加载时运行,后续加载均返回首次运行的结果,除非手动清除系统缓存。
在循环加载场景下,CommonJS属于“加载时执行”。即脚本代码在require时立即执行,若出现模块被“循环加载”的情况,仅输出已执行的部分,未执行部分不会输出。
ES6模块机制详解
ES6模块机制
ES6模块中的值属于【动态只读引用】。
关于“只读”:引入的变量不允许修改其值,无论是基本数据类型还是复杂数据类型。当模块遇到import命令时,会生成一个只读引用。脚本真正执行时,再根据该只读引用去被加载模块中取值。
关于“动态”:原始值发生变化时,import加载的值也会随之变化,适用于基本数据类型和复杂数据类型。
在循环加载场景下,
上述内容阐述了一些重要区别,接下来通过具体示例说明每一点。
CommonJS循环加载与缓存示例
CommonJS示例分析
针对基本数据类型,属于复制行为,即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。
// b.js
let count = 1
let plusCount = () => {
count++
}
setTimeout(() => {
console.log('b.js-1', count)
}, 1000)
module.exports = {
count,
plusCount
}
// a.js
let mod = require('./b.js')
console.log('a.js-1', mod.count)
mod.plusCount()
console.log('a.js-2', mod.count)
setTimeout(() => {
mod.count = 3
console.log('a.js-3', mod.count)
}, 2000)
node a.js
a.js-1 1
a.js-2 1
b.js-1 2 // 1秒后
a.js-3 3 // 2秒后
从代码可以看出,b模块export的count变量是一个复制行为。在plusCount方法调用之后,a模块中的count不受影响。同时,可以在b模块中更改a模块中的值。若希望同步代码,可以export一个getter。
// 其他代码相同
module.exports = {
get count () {
return count
},
plusCount
}
node a.js
a.js-1 1
a.js-2 1
b.js-1 2 // 1秒后
a.js-3 2 // 2秒后, 由于没有定义setter,因此无法对值进行设置。所以还是返回2
针对复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
// b.js
let obj = {
count: 1
}
let plusCount = () => {
obj.count++
}
setTimeout(() => {
console.log('b.js-1', obj.count)
}, 1000)
setTimeout(() => {
console.log('b.js-2', obj.count)
}, 3000)
module.exports = {
obj,
plusCount
}
// a.js
var mod = require('./b.js')
console.log('a.js-1', mod.obj.count)
mod.plusCount()
console.log('a.js-2', mod.obj.count)
setTimeout(() => {
mod.obj.count = 3
console.log('a.js-3', mod.obj.count)
}, 2000)
node a.js
a.js-1 1
a.js-2 2
b.js-1 2
a.js-3 3
b.js-2 3
从代码可以看出,对于对象来说属于浅拷贝。当执行a模块时,首先打印obj.count的值为1,然后通过plusCount方法,再次打印时为2。接着在a模块修改count的值为3,此时在b模块的值也为3。
当使用require命令加载某个模块时,就会运行整个模块的代码。
当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,commonjs模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
3, 4, 5可以使用同一个例子说明
// b.js
exports.done = false
let a = require('./a.js')
console.log('b.js-1', a.done)
exports.done = true
console.log('b.js-2', '执行完毕')
// a.js
exports.done = false
let b = require('./b.js')
console.log('a.js-1', b.done)
exports.done = true
console.log('a.js-2', '执行完毕')
// c.js
let a = require('./a.js')
let b = require('./b.js')
console.log('c.js-1', '执行完毕', a.done, b.done)
node c.js
b.js-1 false
b.js-2 执行完毕
a.js-1 true
a.js-2 执行完毕
c.js-1 执行完毕 true true
下面详细梳理整个执行过程。
在Node.js中执行c模块。此时遇到require关键字,执行a.js中所有代码。
在a模块中exports之后,通过require引入了b模块,执行b模块的代码。
在b模块中exports之后,又require引入了a模块,此时执行a模块的代码。
a模块只执行exports.done = false这条语句。
回到b模块,打印b.js-1, exports, b.js-2。b模块执行完毕。
回到a模块,接着打印a.js-1, exports, b.js-2。a模块执行完毕
回到c模块,接着执行require,需要引入b模块。由于在a模块中已经引入过了,所以直接就可以输出值了。
结束。
从以上结果和分析过程可以看出,当遇到require命令时,会执行对应的模块代码。当循环引用时,有可能只输出某模块代码的一部分。当引用同一个模块时,不会再次加载,而是获取缓存。
ES6动态只读引用与循环加载示例
ES6模块示例分析
es6模块中的值属于【动态只读引用】。以下仅说明复杂数据类型的情况。
对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
对于动态来说,原始值发生变化,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。
// b.js
export let counter = {
count: 1
}
setTimeout(() => {
console.log('b.js-1', counter.count)
}, 1000)
// a.js
import { counter } from './b.js'
counter = {}
console.log('a.js-1', counter)
// Syntax Error: "counter" is read-only
虽然不能将counter重新赋值一个新的对象,但是可以给对象添加属性和方法。此时不会报错。这种行为类型与关键字const的用法。
// a.js
import { counter } from './b.js'
counter.count++
console.log(counter)
// 2
循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。
// b.js
import {foo} from './a.js';
export function bar() {
console.log('bar');
if (Math.random() > 0.5) {
foo();
}
}
// a.js
import {bar} from './b.js';
export function foo() {
console.log('foo');
bar();
console.log('执行完毕');
}
foo();
node a.js
foo
bar
执行完毕
// 执行结果也有可能是
foo
bar
foo
bar
执行完毕
执行完毕
由于在两个模块之间都存在引用。因此能够正常执行。
以上这篇详谈commonjs模块与es6模块的区别就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持本站。
macOS卸载Node.js指南:区分Homebrew与pkg安装包的处理方式
2026-11-05 11:28:12
JS逆向入门:浏览器Console调试与Node.js环境搭建指南
2026-11-05 10:57:15
Node.js 大版本升级需要注意哪些破坏性变更
2026-11-03 20:35:44
Node.js官网LTS版与Current版有什么区别
2026-11-03 19:33:49
Windows彻底卸载Node.js:清理AppData残留与PATH环境变量
2026-11-03 19:15:35
