深入剖析 CommonJS 与 ES Module 模块导出区别
在 JavaScript 的世界里,准确理解 CommonJS 和 ES Module 的模块导出特性,对于开发者而言至关重要。近期,相关讨论引发了深入思考,让我们重新审视二者在值传递上的本质差异。
一直以来,流行着这样一种说法:CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。然而,早期用于证明 CommonJS 模块导出值特性的例程,大多存在瑕疵。以常见的示例来看:
在 b.js 中:
let count = 1;
module.exports = {
count,
add() {
count++;
},
get() {
return count;
}
};
a.js 引入并使用:
const { count, add, get } = require('./b');
console.log(count); // 1
add();
console.log(count); // 1
console.log(get()); // 2
表面上,有人认为此例能说明 CommonJS 特性,但仔细分析,由于 count 变量是基本类型,按值传递,module.exports 中的 count 属性只是获取了初始值的拷贝,后续 count 变量变化与导出的 count 属性毫无关联,所以这个例程并不能严谨地证明 CommonJS 模块导出是值的拷贝。
为求严谨,我们不妨从不同视角来论证。先看 Webpack 构建 CommonJS 模块的情况。假设有如下模块:
a.js:
const b = require('./b');
console.log(b.count);
b.js:
module.exports = {
count: 1,
};
Webpack 输出的 bundle(省去注释和部分无关代码)如下:
(function(modules) {
// webpackBootstrap
//...
// webpack实现的require函数
function __webpack_require__(moduleId) {
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 模块缓存id、加载状态和导出值
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {} // 关键点:模块导出预置了一个空对象
};
// 模块代码执行
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
//...
return __webpack_require__(__webpack_require__.s = 0);
})([
// a.js
(function(module, exports, __webpack_require__) {
const b = __webpack_require__(1);
console.log(b.count);
}),
// b.js
(function(module, exports) {
module.exports = {
count: 1,
};
})
])
从编译后的 bundle 清晰可见,CommonJS 模块导出在 Webpack 这里,实际是对 installedModules[moduleId].exports 属性的赋值操作。当在预置的 installedModules[moduleId].exports 空对象上新增一个基本类型的 count 属性,如同基本类型的拷贝;若 installedModules[moduleId].exports 被赋值一个新的包含 count 属性的对象,就相当于对象浅拷贝。这无疑证明了 CommonJS 模块导出的是值的拷贝。
再从 Node 底层源码角度出发,在 Node 中,当执行 require 操作引入模块时,模块代码被包裹在一个函数中执行,其 exports 对象负责收集对外暴露的值。此过程与 Webpack 处理相似,一旦模块内的变量要作为导出值,不管是基本类型赋值给 exports 的某个属性,还是复杂对象整体赋值给 module.exports,均为单向传递,原始变量后续变化不会反馈到已导出的 “副本” 上。
反观 ES Module,以如下示例说明:
b.mjs:
export let count = 1;
export function add() {
count++;
}
export function get() {
return count;
}
a.mjs:
import { count, add, get } from './b.mjs';
console.log(count); // 1
add();
console.log(count); // 2
console.log(get()); // 2
这里使用 export let 这种声明式导出,模块内变量与导出值紧密绑定,共享内存空间,只要其中一处修改,便能实时反映。这不仅在语法上直观呈现,在引擎底层实现时,基于静态模块结构,模块间的引用关系在解析阶段就已确定,有力保障了引用的一致性与实时性。
综上所述,通过源码分析以及构建工具如 Webpack 的视角,我们已然确凿地证明:CommonJS 模块导出是值的拷贝,ES Module 是值的引用。这一认知为我们深入理解 JavaScript 模块机制奠定了坚实基础,助力开发者在面对复杂项目的模块组织、跨模块优化等场景时,做出更明智的决策。若后续在源码剖析中有全新发现,或是期望进一步探讨它们在更多复杂场景下的应用,不妨继续深入挖掘,持续拓展知识边界。