ES6 模块与 CommonJS 的区别详解

ES6 模块与 CommonJS 的区别详解

文章目录

在 JavaScript 模块化发展的历程中, ES6 模块(ESM)CommonJS(CJS) 是两种极具影响力的模块规范。它们的核心目标都是解决大型项目中代码冗余、作用域混乱、依赖管理复杂等问题,让代码组织更具规范性、可维护性和可复用性。但由于设计理念、诞生背景的不同,二者在语法、加载机制、输出特性等多个维度存在显著差异。本文将从多方面展开详细对比,帮助你清晰理解二者的区别与适用场景。

二、核心差异详解

1. 语法差异

模块规范的核心语法围绕「导入」和「导出」展开,二者的语法风格差异明显,且 CommonJS 更偏向简洁的命令式语法,ES6 模块则采用声明式语法。

CommonJS 语法

CommonJS 使用 require() 函数进行模块导入,通过 module.exportsexports 进行模块导出,语法简洁直观。

javascript 复制代码
// 导出模块:utils.js
// 方式1:整体导出(推荐,语义更清晰)
function sum(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

module.exports = {
  sum,
  multiply
};

// 方式2:单个导出(exports 是 module.exports 的引用,不可直接赋值)
exports.subtract = function(a, b) {
  return a - b;
};

// 导入模块:main.js
const { sum, multiply, subtract } = require('./utils.js');
console.log(sum(2, 3)); // 输出:5
console.log(multiply(2, 3)); // 输出:6
console.log(subtract(3, 2)); // 输出:1
ES6 模块语法

ES6 模块使用 export 关键字进行导出,使用 import 关键字进行导入,支持命名导出和默认导出两种形式,语法更严谨规范。

javascript 复制代码
// 导出模块:utils.js
// 方式1:命名导出(可多个,导入时需与导出名称一致)
export function sum(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// 方式2:默认导出(仅一个,导入时可自定义名称)
export default function subtract(a, b) {
  return a - b;
}

// 导入模块:main.js
// 导入命名导出
import { sum, multiply } from './utils.js';
// 导入默认导出
import subtract from './utils.js';

console.log(sum(2, 3)); // 输出:5
console.log(multiply(2, 3)); // 输出:6
console.log(subtract(3, 2)); // 输出:1

2. 加载时机

加载时机是二者的核心差异之一,直接决定了模块的使用场景和性能表现。

CommonJS:运行时动态加载

CommonJS 模块的加载发生在代码运行阶段 ,而非编译阶段。这意味着 require() 函数可以在代码的任意位置调用,支持条件判断、变量拼接等动态加载逻辑,只有当代码执行到 require() 语句时,才会去加载对应的模块并执行其代码。

javascript 复制代码
// main.js:CommonJS 动态加载示例
const env = 'production';
let utils;

// 支持条件加载
if (env === 'production') {
  utils = require('./utils.prod.js');
} else {
  utils = require('./utils.dev.js');
}

console.log(utils.sum(2, 3));
ES6 模块:编译时静态加载

ES6 模块的加载发生在代码编译阶段 (即预处理阶段),在代码运行前就已经完成模块的解析和依赖收集。这意味着 import 语句必须放在模块的顶层作用域,不能嵌套在 iffor 等代码块中,不支持动态的条件加载逻辑。

javascript 复制代码
// main.js:ES6 模块静态加载示例
// 正确:import 放在顶层作用域
import { sum } from './utils.js';

console.log(sum(2, 3));

// 错误:import 不能嵌套在代码块中(编译阶段会报错)
// const env = 'production';
// if (env === 'production') {
//   import { sum } from './utils.prod.js';
// }

3. 输出机制

输出机制的差异决定了「模块导出的值与原始模块内部值的关联关系」,二者分别采用「值的拷贝」和「值的引用」两种机制。

CommonJS:值的拷贝

CommonJS 模块导出的是值的浅拷贝 ,当模块被 require() 加载时,会将导出对象(module.exports)中的值拷贝一份到导入模块中。后续原始模块内部的变量值发生变化,不会影响已经导入的拷贝值(引用类型拷贝的是引用地址,内部属性变化会同步,变量本身的重新赋值不会同步)。

javascript 复制代码
// counter.js:CommonJS 模块
let count = 0;

function increment() {
  count++;
}

module.exports = {
  count, // 拷贝当前 count 的值(0)
  increment
};

// main.js:导入 CommonJS 模块
const { count, increment } = require('./counter.js');

console.log(count); // 输出:0(初始拷贝值)
increment(); // 调用方法修改 counter.js 内部的 count(变为 1)
console.log(count); // 输出:0(拷贝值不会更新,仍为初始的 0)
ES6 模块:值的引用

ES6 模块导出的是值的只读引用,导入模块获取的不是值的拷贝,而是对原始模块内部变量的引用。原始模块内部的变量值发生变化时,导入模块中的引用值会实时同步更新(同时导入的引用是只读的,不能在导入模块中修改原始模块的变量)。

javascript 复制代码
// counter.js:ES6 模块
export let count = 0;

export function increment() {
  count++;
}

// main.js:导入 ES6 模块
import { count, increment } from './counter.js';

console.log(count); // 输出:0(引用原始模块的 count)
increment(); // 调用方法修改原始模块的 count(变为 1)
console.log(count); // 输出:1(引用值实时更新)

// 错误:导入的引用是只读的,不能直接修改
// count = 10;

4. this 指向

二者模块顶层的 this 指向存在本质区别,这也是判断模块类型的一个重要标识。

CommonJS:this 指向 module.exports 对象

在 CommonJS 模块中,顶层作用域的 this 指向当前模块的 module.exports 对象,通过 this 可以直接修改或添加导出内容。

javascript 复制代码
// utils.js:CommonJS 模块
console.log(this); // 输出:{}(初始为空的 module.exports 对象)
console.log(this === module.exports); // 输出:true

this.hello = function() {
  return 'Hello CommonJS';
};

// main.js
const { hello } = require('./utils.js');
console.log(hello()); // 输出:Hello CommonJS
ES6 模块:this 指向 undefined

ES6 模块采用严格模式(即使未显式声明 'use strict'),顶层作用域的 this 指向 undefined,无法通过 this 访问或修改模块导出内容。

javascript 复制代码
// utils.js:ES6 模块
console.log(this); // 输出:undefined

// 尝试通过 this 添加导出内容(无效)
this.hello = function() {
  return 'Hello ES6 Module';
};

// main.js
import { hello } from './utils.js';
console.log(hello()); // 报错:hello is not a function

5. 循环依赖处理

循环依赖指两个或多个模块互相导入对方(如 A 导入 B,B 又导入 A),二者对循环依赖的处理机制不同,导致最终的执行结果也存在差异。

CommonJS:循环依赖采用部分加载

CommonJS 处理循环依赖时,采用「部分加载」机制:当模块发生循环依赖时,被依赖的模块不会等待完全执行完毕,而是将当前已经执行完成的部分导出内容(module.exports 已赋值的部分)进行拷贝,返回给导入模块,后续未执行的内容不会被同步。

javascript 复制代码
// a.js:CommonJS 模块,导入 b.js
const b = require('./b.js');
console.log('a.js 中的 b:', b);

let aValue = 'a 原始值';

module.exports = {
  aValue,
  updateA: function(newValue) {
    aValue = newValue;
  }
};

// b.js:CommonJS 模块,导入 a.js
const a = require('./a.js');
console.log('b.js 中的 a:', a); // 输出:{ aValue: undefined, updateA: [Function] }(部分加载,aValue 尚未赋值)

module.exports = {
  bValue: 'b 原始值',
  updateB: function(newValue) {
    this.bValue = newValue;
  }
};

// main.js:导入 a.js 触发循环依赖
const a = require('./a.js');
console.log('main.js 中的 a:', a); // 输出:{ aValue: 'a 原始值', updateA: [Function] }
ES6 模块:循环依赖采用动态绑定

ES6 模块处理循环依赖时,采用「动态绑定」机制:由于 ES6 模块导出的是值的引用,而非拷贝,即使发生循环依赖,导入模块获取的也是原始模块的实时引用,当原始模块后续执行完成并更新变量值时,导入模块的引用值会同步更新,无需等待模块完全执行完毕。

javascript 复制代码
// a.js:ES6 模块,导入 b.js
import { bValue, updateB } from './b.js';
console.log('a.js 中的 bValue:', bValue); // 输出:b 原始值(动态绑定,获取实时值)

export let aValue = 'a 原始值';

export function updateA(newValue) {
  aValue = newValue;
}

// b.js:ES6 模块,导入 a.js
import { aValue, updateA } from './a.js';
console.log('b.js 中的 aValue:', aValue); // 输出:undefined(此时 a.js 尚未赋值 aValue)

export let bValue = 'b 原始值';

export function updateB(newValue) {
  bValue = newValue;
}

// main.js:导入 a.js 触发循环依赖
import { aValue, updateA } from './a.js';
console.log('main.js 中的 aValue:', aValue); // 输出:a 原始值
updateA('a 更新值');
console.log('main.js 中的 aValue:', aValue); // 输出:a 更新值

6. 在 Node.js 中的使用

Node.js 作为服务器端 JavaScript 运行环境,对两种模块规范都提供了支持,但使用方式存在差异。

CommonJS:默认支持,无需额外配置

CommonJS 是 Node.js 的默认模块规范,Node.js 诞生之初便采用了 CommonJS 规范,所有 .js 后缀的文件(未特殊配置时)默认被解析为 CommonJS 模块,可直接使用 require()module.exports,无需进行任何额外配置。

javascript 复制代码
// utils.js:Node.js 中默认的 CommonJS 模块
module.exports = {
  sayHello: function() {
    return 'Hello Node.js CommonJS';
  }
};

// main.js:直接运行 node main.js 即可
const { sayHello } = require('./utils.js');
console.log(sayHello()); // 输出:Hello Node.js CommonJS
ES6 模块:两种启用方式

Node.js 对 ES6 模块的支持是后续新增的,默认不启用,需要通过以下两种方式之一手动启用:

  1. 修改文件扩展名为 .mjs :将 ES6 模块文件的后缀改为 .mjs,Node.js 会自动将其解析为 ES6 模块,可直接使用 importexport

    javascript 复制代码
    // utils.mjs:ES6 模块
    export function sayHello() {
      return 'Hello Node.js ES6 Module (.mjs)';
    }
    
    // main.mjs:运行 node main.mjs 即可
    import { sayHello } from './utils.mjs';
    console.log(sayHello()); // 输出:Hello Node.js ES6 Module (.mjs)
  2. 配置 package.jsontype: "module" :在项目根目录的 package.json 中添加 "type": "module" 配置,此时项目中所有 .js 后缀的文件都会被解析为 ES6 模块。

    json 复制代码
    // package.json
    {
      "name": "module-demo",
      "version": "1.0.0",
      "type": "module"
    }
    javascript 复制代码
    // utils.js:ES6 模块(因 package.json 配置,被解析为 ESM)
    export function sayHello() {
      return 'Hello Node.js ES6 Module (package.json)';
    }
    
    // main.js:运行 node main.js 即可
    import { sayHello } from './utils.js';
    console.log(sayHello()); // 输出:Hello Node.js ES6 Module (package.json)

7. 动态导入

虽然 ES6 模块默认是静态加载,但二者都支持动态导入需求,只是实现方式不同。

CommonJS:天然支持动态导入

由于 CommonJS 是运行时加载,require() 函数可以接收动态拼接的路径参数,天然支持动态导入,无需额外语法支持。

javascript 复制代码
// main.js:CommonJS 动态导入示例
function loadModule(moduleName) {
  // 模板字符串拼接模块路径,支持动态导入
  const module = require(`./${moduleName}.js`);
  return module;
}

// 动态加载 utils 模块
const utils = loadModule('utils');
console.log(utils.sum(2, 3)); // 输出:5
ES6 模块:通过 import() 实现

ES6 模块提供了 import() 函数(属于动态导入语法,与静态 import 语句不同),该函数返回一个 Promise 对象,支持通过 .then() 回调或 await 语法处理异步加载结果,实现动态导入需求。

javascript 复制代码
// utils.js:ES6 模块
export function sum(a, b) {
  return a + b;
}

// main.js:ES6 模块动态导入示例
// 方式1:使用 .then() 回调
import('./utils.js').then(({ sum }) => {
  console.log(sum(2, 3)); // 输出:5
});

// 方式2:使用 await(需在异步函数中)
async function loadUtils() {
  const { sum } = await import('./utils.js');
  console.log(sum(2, 3)); // 输出:5
}

loadUtils();

三、总结对比(详细对比表)

特性 CommonJS ES6 模块
加载时机 运行时动态加载 编译时静态加载
输出机制 值的拷贝(浅拷贝) 值的只读引用(动态绑定)
动态性 天然支持动态加载(条件、路径拼接) 静态加载默认不支持,需通过 import() 实现动态导入
this 指向 顶层 this 指向 module.exports 顶层 this 指向 undefined
Tree Shaking 不支持(运行时加载,无法静态分析) 支持(编译时静态分析,可剔除未使用代码)
使用环境 主要用于 Node.js 环境(默认支持) 浏览器环境(原生支持)、Node.js 环境(需配置启用)
顶层 await 不支持(会阻塞模块加载,报错) 支持(可直接在顶层使用,异步加载依赖)
条件导入 支持(可嵌套在代码块中) 不支持静态条件导入,仅 import() 可实现异步条件导入

四、结尾:适用场景与发展趋势

1. 适用场景

  • CommonJS:更适合 Node.js 服务器端开发场景。服务器端模块加载无需考虑浏览器兼容性,且CommonJS 支持动态加载、循环依赖的部分加载机制,更贴合服务器端的动态需求(如根据运行环境加载不同配置),同时 Node.js 对 CommonJS 有完善的默认支持,迁移成本低。
  • ES6 模块:更适合浏览器端开发场景,以及追求高性能、可优化的现代化项目。浏览器端原生支持 ES6 模块,无需额外打包转换(或少量转换),且支持 Tree Shaking 优化,可显著减小打包体积;同时 ES6 模块的静态加载、动态绑定特性,更利于代码的静态分析、类型检查(如 TypeScript 集成),是现代化前端工程(Vue、React 项目)的首选模块规范。

2. 发展趋势

从 JavaScript 生态的发展来看,ES6 模块是标准化、通用性更强的模块规范。它是 ECMAScript 官方制定的标准,具备跨环境(浏览器、Node.js)的通用性,而 CommonJS 是社区制定的非标准规范,仅在 Node.js 环境中占据主导地位。

目前,Node.js 对 ES6 模块的支持越来越完善,现代化前端工程也已全面拥抱 ES6 模块,打包工具(Webpack、Vite)、构建工具、框架都对 ES6 模块提供了深度优化。未来,ES6 模块将逐步成为 JavaScript 生态的主流模块规范,实现「一套模块语法,跨环境运行」的目标,而 CommonJS 则会在 Node.js 旧项目、遗留系统中继续发挥作用,作为 ES6 模块的补充存在。

相关推荐
大猫会长2 小时前
postgreSQL中,RLS的using与with check
开发语言·前端·javascript
摘星编程2 小时前
React Native for OpenHarmony 实战:ProgressBar 进度条详解
javascript·react native·react.js
慧一居士2 小时前
vite.config.ts 配置使用说明,完整配置示例
前端
wusp19942 小时前
nuxt3模块化API架构
前端·javascript·nuxt3
沛沛老爹2 小时前
Web开发者进阶AI:企业级Agent Skills安全策略与合规架构实战
前端·人工智能·架构
摘星编程2 小时前
React Native for OpenHarmony 实战:SegmentControl 分段控件详解
javascript·react native·react.js
摘星编程2 小时前
React Native for OpenHarmony 实战:ProgressRing 环形进度详解
javascript·react native·react.js
遗憾随她而去.2 小时前
前端首屏加载时间的度量:FCP、LCP等指标的规范理解
前端
TAEHENGV3 小时前
React Native for OpenHarmony 实战:数学练习实现
javascript·react native·react.js