详解CommonJS和ES6模块循环加载处理的区别
本文深入解析CommonJS与ES6模块在循环加载处理上的核心差异。CommonJS采用值拷贝机制,加载时执行,循环引用仅返回已执行部分的输出;ES6模块采用动态只读引用,按需取值,能正确处理循环依赖。文章通过Node.js官方案例详细演示了两种规范下的执行顺序与结果,并推荐前端工程使用ES6模块规范。
CommonJS与ES6模块机制概述
在CommonJS模块规范中,开发者通过require语句导入模块,并通过module.exports导出模块。该机制输出的是值的拷贝,这意味着模块导入的也是输出值的拷贝。一旦某个值被输出,模块内部对该值的后续变化将无法被监听或反映到已导出的拷贝中。
ES6模块规范则采用import语句导入模块,通过export语句导出模块。与CommonJS不同,ES6模块输出的是对值的引用。其运行机制在于,当遇到模块加载命令import时,并不会立即执行该模块,而是生成一个动态的只读引用。只有当真正需要获取该值时,系统才会去原模块中取值。因此,如果原始值发生变化,通过引用获取的值也会随之更新。
那么,CommonJS和ES6模块规范在处理模块循环加载时,具体存在哪些机制上的不同呢?
所谓循环加载,是指脚本a的执行依赖于脚本b,而脚本b的执行又反过来依赖于脚本a的情况。
CommonJS模块加载原理与循环加载处理
1. CommonJS模块的加载原理
CommonJS模块本质上是一个脚本文件。当require命令第一次加载该脚本时,会立即执行整个脚本,并在内存中生成该模块的一个说明对象。
{
id: '', //模块名,唯一
exports: { //模块输出的各个接口
...
},
loaded: true, //模块的脚本是否执行完毕
...
}
后续再次使用该模块时,系统会直接到该对象的exports属性中取值。即使再次执行require命令,也不会重新执行模块代码,而是直接从缓存中读取结果。
CommonJS模块属于加载时执行,即脚本代码在require调用时全部执行完毕。如果某个模块被“循环加载”,系统只会输出已经执行过的部分,未执行的部分不会被输出。
案例说明:
案例来源于Node官方说明: https://nodejs.org/api/modules.html#modules_cycles
//a.js
exports.done = false;
var b = require('./b.js');
console.log('在a.js中,b.done = %j', b.done);
exports.done = true;
console.log('a.js执行完毕!')
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在b.js中,a.done = %j', a.done);
exports.done = true;
console.log('b.js执行完毕!')
//main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在main.js中,a.done = %j, b.done = %j', a.done, b.done);
输出结果如下:
//node环境下运行main.js node main.js
在b.js中,a.done = false b.js执行完毕! 在a.js中,b.done = true a.js执行完毕! 在main.js中,a.done = true, b.done = true
JS代码执行顺序如下: 1)main.js中先加载a.js,a脚本先输出done变量,值为false,然后加载b脚本,a的代码停止执行,等待b脚本执行完成后,才会继续往下执行。
2)b.js执行到第二行会去加载a.js,这时发生循环加载,系统会去a.js模块对应对象的exports属性取值,因为a.js没执行完,从exports属性只能取回已经执行的部分,未执行的部分不返回,所以取回的值并不是最后的值。
3)a.js已执行的代码只有一行,exports.done = false;所以对于b.js来说,require a.js只输出了一个变量done,值为false。往下执行console.log('在b.js中,a.done = %j', a.done);控制台打印出:
在b.js中,a.done = false
4)b.js继续往下执行,done变量设置为true,console.log('b.js执行完毕!'),等到全部执行完毕,将执行权交还给a.js。此时控制台输出:
b.js执行完毕!
5)执行权交给a.js后,a.js接着往下执行,执行console.log('在a.js中,b.done = %j', b.done);控制台打印出:
在a.js中,b.done = true
6)a.js继续执行,变量done设置为true,直到a.js执行完毕。
a.js执行完毕!
7)main.js中第二行不会再次执行b.js,直接输出缓存结果。最后控制台输出:
在main.js中,a.done = true, b.done = true
总结:
1)在b.js中,a.js没有执行完毕,只执行了第一行,所以循环加载中,只输出已执行的部分。
2)main.js第二行不会再次执行,而是输出缓存b.js的执行结果。exports.done = true;
ES6模块的循环加载机制
2. ES6模块的循环加载
ES6模块与CommonJS有本质区别,ES6模块是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者保证真正取值时能够取到值,只要引用是存在的,代码就能执行。
案例说明:
//even.js
import {odd} from './odd';
var counter = 0;
export function even(n){
counter ++;
console.log(counter);
return n == 0 || odd(n-1);
}
//odd.js
import {even} from './even.js';
export function odd(n){
return n != 0 && even(n-1);
}
//index.js
import * as m from './even.js';
var x = m.even(5);
console.log(x);
var y = m.even(4);
console.log(y);
执行index.js,输出结果如下:
babel-node index.js
1 2 3 false 4 5 6 true
可以看出counter的值是累加的,ES6是动态引用。如果上面的引用改为CommonJS代码,会报错,因为在odd.js里,even.js代码并没有执行。
//改用CommonJS规范加载文件,执行会报错
var x = m.even(5);
^
TypeError: m.even is not a function
at Object.<anonymous> (/Users/zourong/Projects/node/ES6/mainx.1.js:3:11)
at Module._compile (internal/modules/cjs/loader.js:689:30)
总结与前端工程建议
3. 总结
1)CommonJS模块是加载时执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,没有执行的部分不会输出。
2)ES6模块是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用。
CommonJS模块规范主要适用于后端Node.js,后端Node.js是同步模块加载,所以在模块循环引入时模块已经执行完毕。推荐前端工程中使用ES6的模块规范,通过安装Babel转码插件支持ES6模块引入的语法。
页面内容主要来源于《ES6标准入门》Module 这一章的介绍。如果有描述不清楚或错误的地方,欢迎留言指证。
参考资料:
《ES6标准入门》之Module
Node.js Cycle
ES-Module-Loader
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19
JS对象数组转字符串为什么推荐用JSON.stringify()
2026-11-05 09:40:31
