ES6模块与CommonJS模块核心差异解析
本文深入对比ES6模块与CommonJS模块在加载机制、值传递方式及循环依赖处理上的核心差异。ES6采用编译时静态分析,输出值的引用且支持动态引用;CommonJS为运行时加载,输出值的拷贝。文章通过代码示例详细解析了两者在循环加载时的行为区别及ES6的解决方案,并总结了ES6模块的五大特性。
在ES6规范中,开发者可以通过 import 关键字引入模块,利用 export 关键字导出模块。这种机制相比早期的方案功能更为强大,也是目前业界推荐使用的标准方式。
ES6模块化设计吸收了 CommonJS 和 AMD 模式的优点,不仅语法简洁,还原生支持异步加载。其书写风格与 CommonJS 高度相似,降低了学习成本。
ES6模块的基本用法已广为人知,本文重点在于将其与 CommonJS 进行对比分析,以揭示两者在底层机制上的不同。
ES6与CommonJS的差异
两者主要存在两大核心差异:
- CommonJS模块输出的是值的拷贝,而ES6模块输出的是值的引用。
- CommonJS模块在运行时加载,ES6模块则在编译时确定输出接口。
值拷贝与值引用的区别
// lib/counter.js
var counter = 1;function increment() {
counter++;
}
function decrement() {
counter--;
}
module.exports = {
counter: counter,
increment: increment,
decrement: decrement
};// src/main.jsvar counter = require('../../lib/counter');counter.increment();
console.log(counter.counter); // 1在 main.js 中引入的 counter 实例与原模块内部的 counter 是完全独立的。这解释了为何调用 counter.increment() 后,main.js 中打印的 counter.counter 仍为1。因为引入的变量与模块内部的变量是两个不同的实例,互不影响。
因此,调用 counter.increment() 仅会修改模块内部的 counter 值。若希望修改引入的 counter 变量,需手动执行赋值操作:
counter.counter++;
console.log(counter.counter); // 2相比之下,通过 import 语句引入的模块是实时只读的,直接反映模块内部的状态变化:
// lib/counter.js
export let counter = 1;
export function increment() {
counter++;
}
export function decrement() {
counter--;
}// src/main.js
import * as counter from '../../counter';console.log(counter.counter); // 1
counter.increment();
console.log(counter.counter); // 2加载机制与编译过程
CommonJS 加载的是一个对象(module.exports),该对象仅在脚本运行时生成。而 ES6 模块并非一个对象,而是静态定义,在代码解析阶段即已生成接口。
由于 ES6 模块在编译时输出接口,因此具备以下两个显著特点:
- import 命令会被 JavaScript 引擎进行静态分析,并优先于模块内的其他代码执行。
- export 命令具有变量声明提升的效果,因此 import 和 export 命令在模块中的位置不影响程序的最终输出结果。
// a.js
console.log('a.js')
import { foo } from './b';// b.js
export let foo = 1;
console.log('b.js 先执行');// 执行结果:
// b.js 先执行
// a.js
// a.js
import { foo } from './b';
console.log('a.js');
export const bar = 1;
export const bar2 = () => {
console.log('bar2');
}
export function bar3() {
console.log('bar3');
}// b.js
export let foo = 1;
import * as a from './a';
console.log(a);// 执行结果:
// { bar: undefined, bar2: undefined, bar3: [Function: bar3] }
// a.js循环加载的处理差异
“循环加载”(circular dependency)指脚本a的执行依赖脚本b,而脚本b的执行又依赖脚本a的情况。
// a.js
var b = require('b');// b.js
var a = require('a');若处理不当,循环加载可能导致递归加载,致使程序无法执行,因此应尽量避免出现此类情况。
在 CommonJS 中,脚本代码在 require 时立即全部执行。若出现“循环加载”,模块只输出已执行部分,未执行的部分不会输出。
// 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);运行结果如下:
在 b.js 之中,a.done = false b.js 执行完毕 在 a.js 之中,b.done = true a.js 执行完毕 在 main.js 之中, a.done=true, b.done=true
从上述输出可以看出:
- 在 b.js 执行时,a.js 尚未执行完毕,仅执行了第一行代码。
- 当 main.js 执行到第二行时,不会再次执行 b.js,而是输出缓存的 b.js 执行结果,即其第四行代码。
ES6 处理“循环加载”的方式与 CommonJS 有本质不同。ES6 模块采用动态引用,若使用 import 从模块加载变量(如 import foo from 'foo'),这些变量不会被缓存,而是成为指向被加载模块的引用。开发者需确保在真正取值时能获取到值。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';运行结果如下:
b.mjs ReferenceError: foo is not defined
上述代码中,执行 a.mjs 后会报错,提示 foo 变量未定义。
具体的执行流程如下:
- 执行 a.mjs 后,引擎发现其加载了 b.mjs,因此优先执行 b.mjs,随后再执行 a.mjs。
- 执行 b.mjs 时,已知其从 a.mjs 输入了 foo 接口,此时不会去执行 a.mjs,而是认为该接口已存在,继续向下执行。
- 当执行到第三行 console.log(foo) 时,发现该接口并未定义,从而报错。
解决此问题的方法是确保 b.mjs 运行时 foo 已有定义。这可以通过将 foo 定义为函数来实现。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};最终执行结果为:
b.mjs foo a.mjs bar
ES6模块特性总结
每个模块即使被加载多次,JavaScript 引擎也只执行一次。后续再次加载同目录下同文件时,直接从内存中读取。这意味着一个模块就是一个单例,或者说是一个对象。
代码在模块作用域内运行,而非全局作用域。模块内部的顶层变量对外不可见,不会污染全局作用域。
模块脚本自动采用严格模式,无论是否显式声明 use strict。
模块中可以使用 import 命令加载其他模块(.js 后缀不可省略,需提供绝对 URL 或相对 URL),也可以使用 export 命令输出对外接口。
模块中顶层的 this 关键字返回 undefined,而非指向 window。因此,在模块顶层使用 this 关键字是无意义的。
到此这篇关于浅析ES6与CommonJS中模块的用法对比的文章就介绍到这了,更多相关ES6 CommonJS内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
D3.js 比例尺与坐标轴的配置详解
2026-11-05 16:54:46
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
