JavaScript中CommonJS 与 ES Modules的区别

 更新时间:2026年03月05日 09:29:42   作者:NEXT06  
本文主要介绍了JavaScript中CommonJS 与 ES Modules的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

在前端工程化的演进长河中,模块化规范的变迁是理解 JavaScript 运行机制的关键一环。对于资深开发者而言,CommonJS(简称 CJS)与 ES Modules(简称 ESM)不仅仅是语法的区别,更代表了 JavaScript 在服务端与浏览器端不同运行环境下的架构哲学。

本文将从底层原理出发,剖析这两大规范的核心差异,并结合 Node.js 的最新特性,探讨工程化场景下的互操作性方案。

一、模块化的前世今生

在 ES6 之前,JavaScript 语言层面并没有内置的模块体系。这导致早期的大型项目开发极易陷入全局作用域污染、依赖关系混乱(Dependency Hell)的泥潭。为了解决这一痛点,社区涌现出了多种解决方案。

CommonJS 应运而生,它主要面向服务器端(Node.js)。由于服务器端的文件存储在本地硬盘,读取速度极快,因此 CommonJS 采用了同步加载的设计。这一规范迅速确立了 Node.js 生态的统治地位。

然而,随着前端应用的日益复杂,浏览器端急需一种标准化的模块体系。ES6(ECMAScript 2015)正式推出了 ES Modules。作为官方标准,ESM 旨在统一浏览器和服务器的模块规范,凭借其静态编译和异步加载的特性,逐渐成为现代前端构建工具(如 Vite, Webpack, Rollup)的首选。

二、两大规范的运行机制与特点

1. CommonJS (CJS)

定位:服务器端模块规范,Node.js 的默认模块系统。

核心特点

  • 运行时加载:模块在代码执行阶段才被加载。
  • 同步加载:代码按照编写顺序同步执行,阻塞后续代码直至模块加载完成。
  • 值的拷贝:导出的是值的副本(对于基本数据类型)。

代码示例

// 导出:module.exports
const obj = { a: 1 };
module.exports = obj;

// 引入:require
const obj = require('./test.js');

2. ES Modules (ESM)

定位:ECMAScript 官方标准,旨在实现浏览器与服务端的通用。

核心特点

  • 编译时输出接口:在代码解析阶段(编译时)即可确定依赖关系。
  • 异步加载:支持异步加载机制,适应网络请求环境。
  • 值的引用:导出的是值的动态映射(Live Binding)。

代码示例

// 导出:export
export const obj = { name: 'ESM' };
export default { name: 'Default' };

// 引入:import
import { obj } from './test.js';
import defaultObj from './test.js';

三、深度解析——核心差异

如果要深入理解两者的区别,必须从输出机制、加载时机和加载方式三个维度进行剖析。

1. 输出值的机制:值的拷贝 vs 值的引用

这是 CJS 与 ESM 最本质的区别,也是面试中最高频的考察点。

  • CommonJS:值的拷贝
    CJS 模块输出的是一个对象,该对象在脚本运行完后生成。一旦输出,模块内部的变化就无法影响到这个值(除非导出的是引用类型对象且修改了其属性,这里特指基本数据类型或引用的替换)。
  • ES Modules:值的引用
    ESM 模块通过 export 导出的是一个静态接口。import 导入的变量仅仅是一个指向被导出模块内部变量的“指针”。如果模块内部修改了该变量,外部导入的地方也会感知到变化。

代码演示:

场景:我们定义一个 age 变量和一个自增函数 addAge。

CommonJS 实现:

// lib.js
let age = 18;
module.exports = {
  age,
  addAge: function () {
    age++;
  },
};

// main.js
const { age, addAge } = require('./lib.js');
console.log(age); // 18
addAge();
console.log(age); // 18 (注意:这里依然是 18,因为导出的是 age 变量在导出时刻的拷贝)

ES Modules 实现:

// lib.mjs
export let age = 18;
export function addAge() {
  age++;
}

// main.mjs
import { age, addAge } from './lib.mjs';
console.log(age); // 18
addAge();
console.log(age); // 19 (注意:这里变成了 19,因为 import 获取的是实时的绑定)

技术延伸
由于 ESM 是实时引用,它能更好地处理循环依赖问题。在 ESM 中,只要引用存在,代码就能执行(尽管可能在暂时性死区 TDZ 中);而在 CJS 中,循环依赖可能导致导出一个不完整的对象(空对象),因为模块可能尚未执行完毕。此外,ESM 导入的变量是只读的(Read-only),尝试在 main.mjs 中直接执行 age = 20 会抛出 TypeError。

2. 加载时机:运行时 vs 编译时

  • CommonJS (运行时)
    require 本质上是一个函数。你可以将它放在 if 语句中,或者根据变量动态生成路径。只有当代码执行到这一行时,Node.js 才会去加载模块。

    if (condition) {
      const lib = require('./lib.js'); // 条件加载
    }
    
  • ES Modules (编译时)
    import 语句(静态导入)必须位于模块顶层,不能嵌套在代码块中。JavaScript 引擎在编译阶段(解析 AST 时)就能确定模块的依赖关系。
    工程化价值:这使得 Tree Shaking(摇树优化)  成为可能。构建工具可以在打包时静态分析出哪些 export 没有被使用,从而安全地删除这些死代码,减小包体积。

3. 加载方式:同步 vs 异步

  • CommonJS (同步)
    主要用于服务器端。文件都在本地磁盘,读取时间通常在毫秒级,同步加载不会造成明显的性能瓶颈。
  • ES Modules (异步)
    设计之初就考虑了浏览器环境。在浏览器中,模块需要通过网络请求加载,网络延迟不可控。如果采用同步加载,会阻塞主线程,导致页面“假死”无法交互。因此,ESM 规范规定模块解析阶段是异步的。

四、工程化实践与互操作性

在 Node.js 环境逐步过渡到 ESM 的过程中,两者共存的情况十分常见。

1. 文件后缀与配置

在 Node.js 中,为了区分模块类型:

  • CommonJS:通常使用 .cjs 后缀,或者在 package.json 中未设置 type 字段(默认为 CJS)。
  • ES Modules:强制使用 .mjs 后缀,或者在 package.json 中设置 "type": "module"。

2. 相互引用(Interoperability)

这是开发中最容易踩坑的地方。

场景 A:CommonJS 引用 ES Modules

由于 CJS 是同步的 require,而 ESM 是异步加载的,因此原生 CJS 无法直接 require ESM 文件

  • 常规方案:使用异步的动态导入 import() 配合 IIFE。

    // index.cjs
    (async () => {
      const { default: foo } = await import('./foo.mjs');
    })();
    
  • 新特性(Node.js v22+ / Experimental)
    Node.js 在 2024 年推出了 --experimental-require-module 标志。开启后,支持同步 require 加载 ESM(前提是该 ESM 模块内部没有顶级 await)。

    node --experimental-require-module index.cjs
    

场景 B:ES Modules 引用 CommonJS

ESM 的兼容性较好,可以导入 CJS 模块。

  • 机制:Node.js 会将 CJS 的 module.exports 整体作为一个默认导出(Default Export)处理。

  • 注意事项不支持具名导入(Named Imports)的直接解构。虽然部分构建工具(如 Webpack)支持混用,但在原生 Node.js 环境下,以下写法通常会报错或表现不符合预期:

    // 错误示范 (原生 Node.js)
    import { someMethod } from './lib.cjs'; // 可能会失败,因为 CJS 只有 default 导出
    

    正确写法

    import lib from './lib.cjs';
    const { someMethod } = lib;
    

五、面试场景复盘

面试官提问:“请聊聊 CommonJS 和 ESM 的区别。”

高分回答策略

1. 一句话定性(宏观视角)
“CommonJS 是 Node.js 社区提出的服务器端运行时模块规范,主要特点是同步加载值的拷贝;而 ES Modules 是 ECMAScript 的官方标准,实现了浏览器和服务端的统一,主要特点是编译时静态分析异步加载值的引用。”

2. 核心差异展开(技术深度)
“两者最本质的区别在于输出值的机制
CommonJS 输出的是值的拷贝。一旦模块输出,内部变量的变化不会影响导出值,类似于基本类型的赋值。
ES Modules 输出的是值的引用(Live Binding) 。导入的变量实际上是指向模块内部内存地址的指针,模块内部变化会实时反映到外部,这使得 ESM 能更好地处理循环依赖问题。”

3. 工程化价值(架构视角)
“在工程实践中,ESM 的静态编译特性非常关键。因为它允许构建工具在代码运行前分析依赖关系,从而实现 Tree Shaking,去除无用代码,优化包体积。这是 CommonJS 这种动态加载规范无法做到的。”

4. 兼容性补充(实战经验)
“在 Node.js 环境中,两者互操作需要注意。ESM 可以较容易地导入 CJS(作为默认导出),但 CJS 导入 ESM 通常需要异步的 import()。不过,Node.js 最近引入了 --experimental-require-module 标志,正尝试打破这一同步加载的壁垒。”

到此这篇关于JavaScript中CommonJS 与 ES Modules的区别的文章就介绍到这了,更多相关JavaScript CommonJS ES Modules内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • javascript 关闭IE6、IE7

    javascript 关闭IE6、IE7

    通过javascript关闭浏览器的时候,“一般”会弹出系统的默认提示框,提示用户是否关闭浏览器,这可以通过javascript解决,但是IE6,IE7解决的办法是不一样的
    2009-06-06
  • js加解密 脚本解密

    js加解密 脚本解密

    利用js异或算法实现脚本加解密的代码
    2008-02-02
  • JavaScript跳出循环的常用方法详解

    JavaScript跳出循环的常用方法详解

    这篇文章主要介绍了JavaScript中常用的遍历数组的方式以及如何提前退出循环,常用退出循环的方式包括break、continue、return和throw,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-03-03
  • 使用JavaScript实现一个拖拽缩放效果

    使用JavaScript实现一个拖拽缩放效果

    这篇文章主要介绍了如何使用JS实现一个这样的拖拽缩放效果,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-05-05
  • JavaScript实现酷炫的鼠标拖尾特效

    JavaScript实现酷炫的鼠标拖尾特效

    这篇文章主要为大家介绍了通过JavaScript实现的一个超级好看的鼠标拖尾特效,文中的示例代码讲解详细,对我们学习JavaScript有一定的帮助,感兴趣的可以学习一下
    2021-12-12
  • 利用HTML5+Socket.io实现摇一摇控制PC端歌曲切换

    利用HTML5+Socket.io实现摇一摇控制PC端歌曲切换

    这篇文章主要介绍了利用HTML5+Socket.io实现摇一摇控制PC端歌曲切换,非常具有实用价值,需要的朋友可以参考下。
    2017-01-01
  • 关于JavaScript中name的意义冲突示例介绍

    关于JavaScript中name的意义冲突示例介绍

    这篇文章主要介绍了关于JavaScript中name的意义冲突,需要的朋友可以参考下
    2014-05-05
  • JavaScript跳出循环的几种常用方法总结

    JavaScript跳出循环的几种常用方法总结

    这篇文章主要给大家介绍了关于JavaScript跳出循环的几种常用方法,continue跳过当前迭代,break终止循环,label+break跳出多层循环,return退出函数,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-06-06
  • javascript如何实现create方法

    javascript如何实现create方法

    这篇文章主要介绍了javascript如何实现create方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • JavaScript中fromCharCode 和 fromCodePoint 的详解与应用小结

    JavaScript中fromCharCode 和 fromCodePoint 的详解与应

    本文将详细介绍 JavaScript 中的 String.fromCharCode 和 String.fromCodePoint 方法,这两个方法能够帮助开发者高效地处理字符与编码之间的转换,理解它们的区别与应用,将让你的代码更加灵活和高效,感兴趣的朋友跟随小编一起看看吧
    2024-12-12

最新评论