kizumi_header_banner_img

你好,欢迎来到zizhiの博客!

加载中

文章导读

esm与commonjs


avatar
zizhi 2025年12月7日 195

历史背景

  1. CommonJS (CJS):

    • 诞生背景:诞生于 Node.js。早期 JavaScript 只能在浏览器里运行,没有模块系统。当 Node.js 把它带到服务器端时,必须有一套模块化规范来管理复杂的项目代码。于是,CommonJS 应运而生。
    • 设计目标:为服务器端设计,特点是简单、同步。因为服务器上的文件都在本地硬盘,读取速度很快,所以“同步加载”模块是可以接受的。
  2. ES Modules (ESM):

    • 诞生背景:由 ECMAScript 官方推出的标准。JavaScript 社区意识到语言本身需要一个统一的、官方的模块化标准,既能用于浏览器,也能用于服务器。
    • 设计目标:为所有环境(尤其是浏览器)设计,特点是静态、异步。因为浏览器需要通过网络加载模块,“异步加载”才不会阻塞页面渲染,这是至关重要的。

核心区别

特性 CommonJS (CJS) ES Modules (ESM)
关键字 require / module.exports import / export
加载方式 同步加载 (Synchronous) 异步加载 (Asynchronous)
分析时机 运行时分析 (Runtime) 静态分析 (Static)
值的类型 值的复制 (Value Copy) 动态引用 (Live Binding)
主要环境 Node.js (历史) 浏览器和现代 Node.js

区别:

1. 语法 (Keywords) – 最直观的区别

  • CJS:

    // 导出
    const path = require('path');
    function myFunc() { /* ... */ }
    module.exports = { myFunc }; // 必须导出整个对象
    
    // 导入
    const { myFunc } = require('./myModule.js');
  • ESM:

    // 导出
    import path from 'path';
    export function myFunc() { /* ... */ } // 可以直接导出
    export const PI = 3.14;
    
    // 导入
    import { myFunc } from './myModule.js';

2. 加载方式-最根本的区别

  • CJS (同步): 当 Node.js 遇到 require('./myModule.js') 时,它会停下当前代码的执行,立刻去硬盘读取、解析并执行 myModule.js,然后把 module.exports 的结果返回,之后才继续执行下面的代码。这是一种阻塞式的加载。
  • ESM (异步): 浏览器(或现代 Node.js)看到 import 时,它会发出一个网络请求去获取这个模块。但它不会傻等,而是会继续解析和执行其他代码。模块加载回来后,再执行相关逻辑。这是一种非阻塞式的加载,对用户体验至关重要。

3. 分析时机

  • CJS (运行时): require 本质上是一个函数,你可以在代码的任何地方调用它,甚至可以把它放在 if 语句里,require 的路径也可以是动态拼接的变量。这意味着只有代码执行到那一行,系统才知道需要加载什么模块。

    if (process.env.NODE_ENV === 'development') {
      const devTools = require('./dev-tools'); // 运行时才知道要不要加载
    }
  • ESM (静态): import 是一个关键字,不是函数。它必须写在文件的顶层。这使得打包工具(如 Webpack, Vite)可以在不执行任何代码的情况下,只通过阅读源码,就能分析出整个项目的依赖关系图。这个能力是实现摇树优化 (Tree Shaking) 的基础——即打包时可以安全地移除掉那些你 import 了但从未使用过的代码。


两者之间的异同点

对比一下“单例模式”和“模块级变量”在两个系统中的表现。

相同点:模块缓存机制 & 单例模式

这个核心机制在 CommonJS 和 ESM 中完全相同

无论哪种规范,JavaScript 引擎都会维护一个模块缓存。当一个模块第一次被 requireimport 时,它会被执行一次,然后结果会被缓存起来。之后所有对该模块的 requireimport 都会直接从缓存中读取,不会再次执行模块代码

  • 在 CJS 中: 你可以通过 require.cache 这个对象来查看缓存。
  • 在 ESM 中: 缓存机制是规范的一部分,但通常不直接暴露给开发者。

结论:通过在模块顶层创建一个对象来实现单例模式的原理,在 CommonJS 和 ESM 环境下都完全成立。因为它们都保证了模块只会被初始化一次。

关键差异点:值的复制 与 动态引用

这是两者最微妙、但也最重要的区别。

  • CJS 导出的是“值的复制”: 当 require 一个模块时,你得到的是那个模块 module.exports 对象在那一刻的一个快照副本。如果被导入的模块内部之后改变了导出的值,导入方是不知道的。

    counter.cjs (CommonJS)

    let count = 0;
    function increment() {
      count++;
      console.log(`(Inside CJS) Count is now ${count}`);
    }
    module.exports = {
      count: count, // 导出的是此刻 count 的值,也就是 0
      increment: increment,
    };

    main.cjs

    const counter = require('./counter.cjs');
    
    console.log(`(Outside CJS) Initial count: ${counter.count}`); // 输出 0
    counter.increment(); // 输出 (Inside CJS) Count is now 1
    console.log(`(Outside CJS) Count after increment: ${counter.count}`); // 还是输出 0!

    为什么?

    因为 main.cjs 得到的 counter.count 只是 这个值的拷贝。counter.cjs 内部的 count 变量变了,但 main.cjs 手里的那份拷贝不会更新。

  • ESM 导出的是“动态引用” (Live Binding): 当你 import 一个成员时,你得到的是一个指向原始模块内部那个变量的“实时指针”或“只读视图”。如果被导入的模块内部改变了这个变量的值,导入方能立即看到这个变化

    counter.mjs (ESM)

    export let count = 0;
    export function increment() {
      count++;
      console.log(`(Inside ESM) Count is now ${count}`);
    }

    main.mjs

    import { count, increment } from './counter.mjs';
    
    console.log(`(Outside ESM) Initial count: ${count}`); // 输出 0
    increment(); // 输出 (Inside ESM) Count is now 1
    console.log(`(Outside ESM) Count after increment: ${count}`); // 输出 1!

    为什么? 因为 main.mjs 里的 count 不是一个值的拷贝,它就像一个直接连通到 counter.mjs 内部 count 变量的窗口,能实时反映其值的变化。

总结

CommonJS (CJS) ES Modules (ESM)
设计初衷 服务器端,文件 I/O 很快 浏览器端,网络 I/O 很慢
加载方式 同步、阻塞 异步、非阻塞
分析时机 运行时,动态 编译时,静态
优化 难以做 Tree Shaking 天然支持 Tree Shaking
单例模式 支持 (通过模块缓存) 支持 (通过模块缓存)
值传递 导出的是值的拷贝 导出的是动态引用 (Live Binding)

前端开发中,接触到的几乎都是ESM,因为打包工具(Vite, Webpack)都是基于 ESM 的静态分析能力来做各种优化的。在写 Node.js 后端或者一些老的配置文件时,可能还会遇到 CommonJS,因为vite和webpack的开发服务器是使用了nodejs的不同框架,运用的通常是commonjs规范



评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码
zizhi的小站

个人信息

avatar

zizhi

一个迷茫的前端er

3
文章
1
评论
1
用户