CommonJS与ES6 Module模块规范详解与核心区别

作者:岩茶

时间:2026-10-25

浏览:422

本文深入解析Node.js中两种主流模块规范:CommonJS与ES6 Module。详细对比了两者在导出语法(module.exports与export)、导入方式(require与import)上的具体用法,并重点剖析了模块依赖关系的动态与静态差异,以及值拷贝与值引用的本质区别,帮助开发者掌握正确的模块化编程实践。

文章目录

前言

在学习JavaScript并实际使用Node.js进行开发后,开发者应当对模块化机制有了基本认知。在实际项目中,通常会接触到两种截然不同的模块导入与导出实现方式。

第一种实现方式基于ES6 Module规范。

// B.js
function show() {
	console.log('show方法被执行')
}
export default show
// A.js
import show from './B.js'
show()  // show方法被执行

第二种实现方式基于CommonJS规范。

// B.js
function show() {
	console.log('show方法被执行')
}
module.exports = {
	show
}
// A.js
const bModule = require('./B.js')
bModule.show()  // show方法被执行

上述两种导入导出机制分别对应了JavaScript生态中两种核心的模块规范标准:ES6 Module与CommonJS。

本文将详细阐述这两种规范在具体使用场景中的差异,并深入分析其底层行为的不同。

1. CommonJS规范

CommonJS标准由JavaScript社区于2009年提出,旨在为JavaScript提供模块化支持。该标准随后被Node.js环境采纳并实现,这意味着在Node.js环境中进行的模块导入与导出操作,均遵循CommonJS规范。

1.1 模块导出机制

在CommonJS中,每个文件被视为一个独立的模块,模块间相互隔离,互不干扰。当其他模块需要使用特定功能时,只需在代码中显式导入目标模块即可。

若希望模块内容被其他模块访问,首先需将其变量或方法显式导出。在CommonJS规范下,导出操作主要有两种语法形式。

// B.js
// 定义了函数show
function show() {
	console.log('show方法被调用')
}
// 定义了变量count
let count = 3
/*--------------  导出方法  --------------*/
// 第一种
module.exports = {
	show,
	count
}
// 第二种
exports.show = show
exports.count = count

从代码逻辑上看,上述两种导出方式在功能上是等价的。

第一种导出方式是将需要对外暴露的函数或变量挂载到 module.exports 属性上。需要注意的是,module.exports 在模块初始化时原本是一个空对象。

在第二种导出方式中,exports 在内部实际上是指向 module.exports 的引用。因此,当我们执行 exports.变量 或 exports.函数 时,本质上是将变量或函数存储到了 module.exports 中。

注意:这里要特别强调的是,在使用第二种导出方式时,不能对 exports 进行重新赋值,否则就将 module.exports 直接全部覆盖了

1.2 模块导入机制

接下来分析CommonJS的导入语法实现。

// A.js
const bModule = require('./B.js')
console.log(bModule.count)  // 3
bModule.show()  // show方法被调用

从代码示例可以看出,CommonJS通过 require 函数来加载模块,该函数接收模块文件的路径作为参数。需要特别注意的是,导入模块后接收到的返回值是一个对象,即 module.exports 的值,开发者需从该对象中解构获取所需的变量或函数。

此外,require 函数具有一个特殊特性:它还可以接收一个表达式作为参数,示例代码如下。

let fileName = 'B.js'
const bModule = require('./' + fileName)

这意味着,开发者可以在运行时动态地改变并决定模块的加载路径,从而实现条件加载或动态模块解析。

2. ES6 Module规范

如标题所示,ES6 Module标准是在ES6(ECMAScript 2015)版本中正式提出的,这标志着JavaScript原生具备了模块化特性。

2.1 模块导出机制

在ES6 Module中,导出操作使用了 export 关键字。根据使用场景,导出方式主要分为命名导出(Named Exports)和默认导出(Default Export)两种。

第一种情况:命名导出

// B.js
/*--------  单个变量或函数导出  ----------*/
export function show() { console.log('show方法被调用') }
export let count = 3
/*--------  批量导出  ----------*/
function show() { console.log('show方法被调用') }
let count = 3
export {show, count}

上述代码展示了两种写法,这两种写法在功能上是等价的。

第一种方式用于导出单个变量或函数,只需在定义前直接使用 export 关键字即可。

第二种方式用于批量导出多个变量或函数,只需将它们封装在一个对象中,并使用 导出该对象即可。

第二种情况:默认导出

// B.js
function show() { console.log('show方法被调用') }
// 命名导出变量count
export let count = 3
// 默认导出函数show
export default show

默认导出是在 export 关键字后紧跟 default 关键字,表示导出的该变量或函数是匿名的,没有具体的名称。

注意:一个模块中只能使用一次默认导出,否则会导致语法错误。具体原因将在后文关于模块加载机制的对比中进行解释。

2.2 模块导入机制

ES6 Module的导入操作使用了 import 关键字,具体语法如下。

// A.js
import {show, count} from './B.js'
show()   // show方法被调用
console.log(count)  // 3

ES6 Module的导入需要使用一对 {} 大括号来接收需要导入的方法或函数,这被称为解构导入。

注意:大括号中的变量或函数名称必须与导出时的名称完全一致,否则无法正确获取值。

如果希望修改导入的变量或函数的名称,可以通过 as 关键字进行重命名,示例代码如下。

// A.js
import {show as print, count as number} from './B.js'
print()   // show方法被调用
console.log(number)  // 3

如果希望将模块中所有导出的变量或函数一次性导入,可以使用 * 通配符进行整体导入,示例代码如下。

import * as bModule from './B.js'
bModule.show()  // show方法被调用
console.log(bModule.count)  // 3

* 表示匹配所有导出项,我们将所有导出项导入并赋值给 bModule 对象。之后,我们可以通过 bModule 对象访问到任何想要的变量或函数。

以上讨论均针对命名导出的变量或函数,那么如何导入默认导出的变量或函数呢?

// 将通过 export default 导出的变量导入
import print from './B.js'
print()  // show方法被调用

命名导出通过 {} 接收,而去掉 {} 后接收的则是默认导出的变量。由于默认导出的变量是匿名的,因此我们可以随意指定一个变量名用于接收该值。

补充说明:与CommonJS不同,ES6 Module的导入文件路径不支持表达式,即路径必须是静态字符串。

3. CommonJS与ES6 Module的核心区别

这两种规范的主要区别体现在以下两个关键方面:

  • 对于模块依赖关系的处理,CommonJS是动态的,而ES6 Module是静态的。
  • 在数据传递机制上,CommonJS导入的是值的拷贝,而ES6 Module导入的是值的引用。

3.1 区别一:动态与静态依赖

关于模块依赖,何为“动态”?何为“静态”?

动态依赖是指模块的依赖关系建立在代码执行阶段。

静态依赖是指模块的依赖关系建立在代码编译或解析阶段。

前文提到,CommonJS导入时,require 函数的路径参数支持表达式,例如。

// A.js
let fileName = 'example.js'
const bModule = require('./' + fileName)

由于该路径在代码执行时是可以动态改变的,因此如果在代码编译阶段就建立各个模块的依赖关系,将无法保证准确性。只有在代码运行后,才能真正确认模块的依赖关系,因此称CommonJS为动态加载。

理解了这一点,也就明白了为什么ES6 Module被称为静态加载:其导入路径在编译时确定,无法动态改变。

3.2 区别二:值拷贝与值引用

为了验证这一区别,下面通过具体实例进行演示。

首先验证CommonJS的行为,代码如下。

// B.js
let count = 3
function change() {
    count ++    // 变量count + 1
    console.log('原count值为:', count);  // 打印B.js模块中count的值
}
module.exports = {
    count,
    change
}
// A.js
let count = require('./B.js').count 
let change = require('./B.js').change
console.log('改变前:', count);   
change()     // 调用模块B.js中的change方法,将原来的count + 1
console.log('改变后:', count); 

运行A.js文件后的输出结果。

改变前:3 原count值为:4 改变后:3

在上述代码中,A.js 文件导入了 B.js 文件中的变量 count 和函数 change。由于CommonJS导入的 count 只是对原有值的一个拷贝,因此尽管我们调用了函数 change 改变了 B.js 文件中变量 count 的值,也不会影响到 A.js 文件中的变量 count。

根据上述运行结果得出结论:CommonJS导入的变量是对原值的拷贝。

接下来验证ES6 Module的行为,代码如下。

// B.js
let count = 3
function change() {
    count ++        // 变量count + 1
    console.log(count);   // 打印B.js模块中count的值
}
export {count, change}
// A.js
import {count, change} from './B.js';
console.log('改变前:',count);
change()         // 调用模块B.js中的change方法,将原来的count + 1
console.log('改变后:', count);

运行A.js文件后的输出结果。

改变前:3 原count值为:4 改变后:4

与CommonJS的结果不同,ES6 Module导入的变量 count 会随着原值的改变而改变。

根据上述运行结果得出结论:ES6 Module导入的变量是对原值的引用。

4. 总结

本文到此结束。关于CommonJS和ES6 Module,还有一些本文未提及的高级知识,建议读者深入查阅官方文档或相关技术文章以获取更全面的信息。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章