第 30 题:模块化原理(CommonJS vs ESModule)

好的,我们从你指定的 第 35 题:模块化原理(CommonJS vs ESModule) 开始。

我会给你 详细版 + 面试官思维 + 易错点 + 速记卡片


第 35 题:CommonJS 与 ES Module 的区别?原理是什么?为什么 Node 现已逐步推荐 ESM?


一、CommonJS 与 ESModule 的核心区别(速记)

特性 CommonJS(CJS) ES Module(ESM)
语法 require() / module.exports import / export
加载方式 同步加载 异步加载(编译期加载)
执行时机 运行时加载 编译时加载(静态分析)
导出值是否可变 导出的值是拷贝(值拷贝) 导出的是引用(live binding)动态更新
能否 tree shaking ❌ 不支持 ✅ 支持(静态分析决定)
是否支持顶层 await ❌ 不支持 ✅ 支持
是否允许循环依赖 可以但复杂 可以而且天然处理更好
Node 支持情况 默认支持 Node 16+ 完整稳定支持

二、深入原理:为什么两者的行为不同?


1)加载机制不同(同步 vs 异步)

🔸 CommonJS:运行时加载(sync)

  • require() 相当于:

    1. 读取文件内容
    2. 执行整个文件(形成 module 对象)
    3. 返回 module.exports

它必须等文件执行完才能继续,所以是 同步阻塞

适合本地文件模块,不适合浏览器(因为同步阻塞网络)


🔸 ESModule:编译时加载(async)

  • 在代码运行前,JS 引擎会对 import静态分析
  • 不会执行代码,只会记录模块依赖图
  • 之后按依赖图异步加载执行

静态分析意味着引擎能提前知道你用了哪些东西(支持 tree shaking)。


2)导出值是"值拷贝" vs "引用绑定"

🔸 CommonJS:值拷贝缓存

ini 复制代码
exports.count = 1;

setTimeout(() => {
  exports.count++;
}, 1000);

另外一个文件:

javascript 复制代码
const { count } = require('./a');
console.log(count); // 永远是 1,不会变成 2

因为 CommonJS 返回的是 第一次 require 时的 module.exports 快照


🔸 ESModule:引用绑定(live binding)

ini 复制代码
export let count = 1;
setTimeout(() => count++, 1000);

导入者:

javascript 复制代码
import { count } from "./a.js";

console.log(count); // 1
setTimeout(() => console.log(count), 1500);
// 输出 2(实时更新)

ESM 导入的是变量引用,本质是 getter。


3)Cirular dependency(循环依赖)处理完全不同

🔸 CommonJS:执行整个文件 → 才有 module.exports

循环依赖时会出现"部分加载",很容易出现 undefined。


🔸 ESModule:因为静态分析

ESM 能在执行前构建依赖图,所以即使循环依赖也能正常引用。

浏览器和 Node 都按标准执行,不容易出问题。


4)Tree Shaking 能否生效

ESM 能,CJS 不行。

这是因为:

  • CJS 运行时决定导出内容(动态特性) → 无法静态分析
  • ESM 导出结构固定 → 能在构建期删除没用的代码

所以现代前端构建工具都会要求你尽量使用 ESModule。


三、Node 为什么推荐 ESModule?(面试官爱问)

原因有五:

1)浏览器与 Node 的统一标准

以前 Node 推 CJS,浏览器没有模块化 → 必须用 webpack 等打包

如今浏览器支持 ESM → Node 也对齐标准。


2)静态分析能力更强 → 更好优化

比如:

  • tree shaking
  • Scope Hoisting
  • 更快的加载速度
  • 更强可预测性

3)天然支持顶层 await

csharp 复制代码
const data = await fetch(...)

CJS 完全无法做到。


4)更好的循环依赖处理


5)更安全、更可控的模块加载系统

Node 的 CJS 模块解析路径复杂、易冲突

ESM 则完全按 URL 和标准路径执行。


四、前端最爱考的面试题:为什么这段代码打印不一样?

例 1:CommonJS

ini 复制代码
// a.js
let counter = 1;
setTimeout(() => counter++, 1000);
module.exports = { counter };

// b.js
const a = require('./a');
console.log(a.counter);
setTimeout(() => console.log(a.counter), 1500);

输出:

复制代码
1
1   ← 不会变

例 2:ESM

javascript 复制代码
// a.js
export let counter = 1;
setTimeout(() => counter++, 1000);

// b.js
import { counter } from './a.js';
console.log(counter);
setTimeout(() => console.log(counter), 1500);

输出:

复制代码
1
2  ← 会变

五、速记卡片(给你每天复习用)


🟦 🌟 模块化:CJS vs ESM 速记卡片

CommonJS(Node 老体系)

  • require() 同步加载
  • 运行时决定依赖
  • 导出是"值拷贝",使用缓存
  • 不支持顶层 await
  • 无法使用 tree shaking
  • 循环依赖易出问题

ES Module(现代体系)

  • import 异步、编译时分析
  • 导出是"引用绑定"
  • 支持顶层 await
  • 可 tree shaking
  • 循环依赖天然支持
  • 浏览器原生支持,Node 推荐

如果你愿意,我可以继续「第 36 题」

👉 Tree Shaking 原理与常见失效原因(非常高频)

要继续吗?

相关推荐
前端一课1 小时前
第 26 题:Vue2 和 Vue3 的响应式原理有什么区别?为什么 Vue3 要用 Proxy 替代 defineProperty?
前端·面试
前端一课1 小时前
第 31 题:Tree Shaking 原理与常见失效原因(高频 + 难点 + 面试必考)
前端·面试
前端一课1 小时前
第 27 题:Promise 实现原理(含手写 Promise)
前端·面试
前端一课1 小时前
第 32 题:深入理解事件循环(Event Loop)、微任务、宏任务(详细 + 难点 + 易错点)
前端·面试
前端一课1 小时前
【前端每天一题】🔥 第 25 题:什么是 Virtual DOM?它的优缺点是什么?Diff 算法是如何工作的?
前端·面试
前端一课1 小时前
【前端每天一题】第 23 题:闭包(Closure)与作用域链(详细 + 面试模板 + 速记卡)
前端·面试
前端一课1 小时前
【前端每天一题】🔥第 22 题:HTTP vs HTTPS、TCP vs UDP 的区别
前端·面试
前端一课1 小时前
第 26 题:浏览器与 Node.js 的事件循环有什么区别?
前端·面试
前端一课1 小时前
【前端每天一题】🔥 第 24 题:Virtual DOM 中 diff 算法的核心流程(详细版
前端·面试