CommonJs、esModules 深入浅出 
1. 为什么会有 CommonJs 和 esModules 
在早期 JavaScript 开发中,开发者通常使用全局变量来定义和使用模块。这种方式虽然简单,但在大型项目中容易导致变量污染和命名冲突。为了解决这个问题,开发者开始使用模块系统来组织代码,确保每个模块的变量和函数不会相互干扰。
1.1 主要问题: 
- 变量污染:js 文件作用域都是顶层,导致变量冲突。
 - js 文件多,需要手动维护依赖关系。
 - 文件依赖问题,若引入顺序错误,会导致报错。 为解决以上问题,javascript 社区出现了 CommonJS 和 ES Modules 是 JavaScript 中两种主要的模块系统。它们的出现是为了解决模块化开发中的问题,提供了一种更结构化和可维护的代码组织方式。 解决变量污染问题:
 
- 每个文件都是独立的作用域,所以不存在变量污染问题。 解决代码维护与文件依赖问题:
 - 通过 import/require 语句明确声明依赖关系
 - 模块系统会自动解析和维护依赖树
 - 避免手动管理文件加载顺序
 - 支持按需加载,提高性能
 
2. Commonjs 
Commonjs 弥补了 javascript 没有模块化,没有统一标准的缺陷。 nodejs 借鉴了 Commonjs 规范,实现了模块化。 Commonjs 规范规定,一个单独的文件就是一个模块,每个模块都是一个单独的作用域;每个模块都有独自的独立作用域,模块与模块之间互不干扰,并且每个模块中定义的变量、函数、对象都是私有的,对其他模块不可见。
2.1 CommonJS 应用场景 
- Node.js 是 CommonJS 的主要实现,Node.js 环境中默认使用 CommonJS 规范
 - Browserify 是一个打包工具,可以将 CommonJS 模块打包成浏览器可识别的代码
 - Webpack 支持 CommonJS 模块规范,可以处理和打包 CommonJS 模块
 
2.2 CommonJS 语法 
- 导入模块:js
const module = require('./module.js') - 导出模块:js
// 方式一:使用 module.exports module.exports = { method1, method2 } // 方式二:使用 exports exports.method1 = method1 exports.method2 = method2 
使用特点
- 在 Commonjs 中每一个 js 文件都是一个独立的模块,每个模块都是一个单独的作用域。
 - 包含 Commonjs 规范的核心变量是: module、exports、require。
 - require 是 Commonjs 规范中用于导入模块的函数。
 
3. CommonJS 实现原理 
CommonJS 的实现原理是一个非常精巧的设计,它通过模块包装器(Module Wrapper)和巧妙的对象引用机制来实现模块的隔离与导出。
3.1 模块包装机制 
当我们编写一个 CommonJS 模块时,我们的代码实际上会被 Node.js 包装在一个函数里面。这个包装函数提供了私有的作用域,同时注入了一些重要的变量:
(function(exports, require, module, __filename, __dirname) {
    // 我们编写的模块代码会被放在这里
})这个包装函数非常关键,它:
- 创建了一个私有的作用域,避免了全局变量的污染
 - 注入了 module 和 exports 对象,使得模块能够导出内容
 - 提供了 require 函数,允许模块导入其他模块
 - 提供了文件相关的变量 __filename 和 __dirname
 
3.2加载机制详解 
Node.js 的模块加载过程是同步的,按照以下步骤执行:
路径解析:
- 首先解析模块路径,支持多种格式(相对路径、绝对路径、包名)
 - 查找 node_modules 目录
 - 处理文件扩展名(.js、.json、.node)
 
缓存检查:
- Node.js 维护了一个模块缓存系统
 - 首次加载时,模块会被执行并缓存
 - 后续加载同一模块时直接返回缓存结果
 
// 简化版的模块加载实现
function require(moduleId) {
    // 1. 检查模块是否已经被加载
    if (require.cache[moduleId]) {
        return require.cache[moduleId].exports;
    }
    // 2. 创建模块对象
    const module = {
        exports: {},
        loaded: false,
        id: moduleId
    };
    // 3. 将模块放入缓存
    require.cache[moduleId] = module;
    // 4. 加载模块
    loadModule(moduleId, module, module.exports);
    // 5. 返回导出的内容
    return module.exports;
}3.3 导出机制的实现 
CommonJS 的导出机制基于 module.exports 对象:
引用传递:
- module.exports 初始为空对象 {}
 - exports 变量是 module.exports 的引用
 - 最终导出的是 module.exports 指向的对象
 
值拷贝:
- 导入模块时得到的是值的拷贝,而不是引用
 - 这意味着导入的值不会随导出模块的变化而变化
 
// 在模块内部
var module = { exports: {} };
var exports = module.exports;
// 这样赋值是可以的
exports.hello = function() {};
// 这样会断开 exports 与 module.exports 的引用
exports = { hello: function() {} }; // 不推荐
// 这样才是正确的完整重新赋值方式
module.exports = { hello: function() {} };3.4 循环依赖处理 
CommonJS 通过返回"未完成"的导出来处理循环依赖:
- 模块在执行时标记为"正在加载"
 - 如果在加载过程中遇到循环依赖
 - 返回当前已执行的部分结果
 - 继续执行未完成的加载过程
 
这种机制可能导致一些模块拿到的是不完整的导出,需要开发者特别注意。
3.5 require 文件加载流程 
3.5.1 模块分类 
require 可以加载三种类型的模块:
const fs = require('fs')           // ① 核心模块
const hello = require('./hello.js') // ② 文件模块
const crypto = require('crypto-js') // ③ 第三方模块流程图:
接收模块标识符
      ↓
判断标识符类型
      ↓
┌─────┴─────┬────────┐
核心模块     路径模块   第三方模块
(fs/http)   (./ ../ /)  (其他)
   │           │         │
优先加载    解析真实路径   遍历node_modules
   │           │         │
返回内置    按扩展名查找   查找package.json
   │           │         │
   └─────┬────┴─────────┘
         ↓
    返回模块内容3.5.2 模块标识符处理原则 
- 核心模块:如 
fs、http等,优先级最高 - 路径模块:以 
./、../、/开头的模块 - 第三方模块:非路径且非核心模块
 
3.5.3 加载流程图 
开始 require(id)
      ↓
检查 Module._cache
      ↓
   是否缓存? ──是─→ 返回 module.exports
      ↓ 否
  解析模块路径
      ↓
创建新的 module 对象
      ↓
将 module 加入缓存
      ↓
  加载并执行模块
      ↓
module.loaded = true
      ↓
返回 module.exports3.5.4 模块查找顺序 
核心模块:
- 直接返回编译好的内置模块
 
文件模块:
require('./module')
         ↓
  检查 module.js
         ↓
是否存在?──否──┐
    │是         ↓
    ↓      检查 module.json
 加载执行        ↓
    │      是否存在?──否──┐
    │          │是        ↓
    │          ↓     检查 module.node
    │       加载执行        ↓
    │          │      是否存在?──否──→ 抛出错误
    │          │          │是
    │          │          ↓
    └──────────┴──────→ 加载执行- 第三方模块:
 
require('module-name')
         ↓
当前目录/node_modules
         ↓
  是否找到模块?───是──→ 返回模块
         ↓ 否
  向上级目录查找
         ↓
父级/node_modules
         ↓
  是否找到模块?───是──→ 返回模块
         ↓ 否
继续向上查找直到根目录
         ↓
  抛出模块未找到错误3.5.5 避免循环引用的机制 
// a.js 加载 b.js 时的过程
require('b.js')
  ↓
将 b.js 加入缓存 // 关键步骤!
  ↓
执行 b.js
  ↓
b.js 尝试 require('a.js')
  ↓
发现 a.js 在缓存中
  ↓
直接返回未完成的 a.js 的 exports3.5.6 核心实现代码 
function require(id) {
    // 1. 检查缓存
    const cachedModule = Module._cache[id]
    if (cachedModule) {
        return cachedModule.exports
    }
    // 2. 创建模块
    const module = {
        exports: {},
        loaded: false,
        id: id
    }
    // 3. 缓存模块
    Module._cache[id] = module
    // 4. 加载模块
    loadModule(id, module, module.exports)
    // 5. 标记完成
    module.loaded = true
    // 6. 返回导出
    return module.exports
}3.6 require 动态加载 
CommonJS 的 require 是同步加载的,但它也支持动态加载。这意味着我们可以在代码运行时根据条件来决定加载哪个模块:
const moduleName = require('./module.js')
if (condition) {
    moduleName = require('./module2.js')
}4. export 和 module.exports 的区别与使用 
4.1 exports 和 module.exports 的关系 
- exports 是 module.exports 的引用
 - 初始时 exports = module.exports = {}
 - 最终导出的是 module.exports 指向的对象
 
4.2 exports = {} 直接赋值的问题 
- 这会切断 exports 与 module.exports 的引用关系
 - module.exports 仍然指向原来的空对象
 - 导致导出的内容仍是空对象
 
4.3 exports 的正确使用方式 
// 1. 正确:通过属性赋值
exports.name = 'value'
exports.method = function() {}
// 2. 正确:通过 module.exports 整体赋值
module.exports = {
    name: 'value',
    method: function() {}
}
// 3. 错误:直接赋值会断开引用
exports = {  // 这样会导致导出失败
    name: 'value'
}4.4 内部实现原理 
// Node.js 模块系统内部实现简化版
function require(/* ... */) {
    // 1. 创建模块对象
    const module = { exports: {} }
    
    // 2. 创建 exports 引用
    const exports = module.exports
    
    // 3. 执行模块代码(在这里可能会修改 exports)
    (function(exports, require, module) {
        // 模块代码在这里执行
    })(exports, require, module)
    
    // 4. 返回 module.exports(而不是 exports)
    return module.exports
}4.5 使用建议 
保持一致性
- 在同一模块中,选择一种导出方式并坚持使用
 - 不要混用 exports.x 和 module.exports 赋值
 
避免同时使用
javascript// 不推荐 exports.name = 'value' module.exports = { method: function() {} }推荐写法
javascript// 方式一:属性导出 exports.name = 'value' exports.method = function() {} // 方式二:整体导出 module.exports = { name: 'value', method: function() {} }
4.6 常见陷阱 
导出空对象
javascriptexports = { name: 'value' } // 错误:模块会导出空对象 console.log(module.exports) // {}引用丢失
javascript// 错误示例 exports = function() {} // 引用丢失 exports.name = 'value' // 无效赋值 // 正确示例 module.exports = function() {} module.exports.name = 'value'异步赋值
javascript// 注意:异步赋值可能导致意外结果 setTimeout(() => { exports.name = 'value' // 如果在 require 之后,导入方无法获取此值 }, 1000)
4.7 为什么同时存在 exports 和 module.exports? 
历史原因:
- Node.js 早期只有 module.exports
 - exports 是为了提供更简便的写法而创建的别名
 - exports = module.exports 的设计让开发者可以少写 "module."
 
使用便利性:
javascript// 使用 module.exports 较繁琐 module.exports.method1 = function() {} module.exports.method2 = function() {} // 使用 exports 更简洁 exports.method1 = function() {} exports.method2 = function() {}
4.8 module.exports 的局限性 
整体赋值会覆盖之前的导出:
javascriptmodule.exports.method1 = function() {} module.exports = { method2: function() {} } // method1 被覆盖掉了不支持渐进式导出:
javascript// 当需要分散在多处导出时,module.exports 不够灵活 if (condition) { module.exports = { /* ... */ } // 会覆盖之前的导出 } else { module.exports = { /* ... */ } // 也会覆盖 }代码组织性差:
javascript// 使用 exports 可以就近导出,更清晰 function method1() {} exports.method1 = method1 function method2() {} exports.method2 = method2 // 使用 module.exports 往往需要集中导出,可读性较差 function method1() {} function method2() {} module.exports = { method1, method2 }重构成本高:
javascript// 如果使用 module.exports 整体导出,添加或删除方法需要修改导出对象 module.exports = { method1, method2, // 添加新方法需要修改这里 } // 使用 exports 则更灵活 exports.method1 = method1 exports.method2 = method2 // 直接添加新方法即可,无需修改其他代码 exports.method3 = method3
5. ES Modules 
5.1 基本概念 
ES Modules (ESM) 是 JavaScript 官方的标准模块系统,具有以下特点:
静态性:
- 导入导出语句只能在模块顶层
 - 导入导出的模块路径必须是字符串常量
 - 在编译时就能确定模块的依赖关系
 
异步加载:
- 模块文件的加载是异步的
 - 执行是在所有模块加载完成之后
 
优势
- 借助 Es Modules 的静态导入导出的优势,实现了 tree-shaking 优化,可以有效减少打包后的代码体积。
 - 利用 import() 懒加载方式实现了代码分割
 
5.2 基本语法 
Es6 Modules 的导入和导出都是静态的,import 会自动提升到代码顶层,而 require 是动态的,需要等到运行时才能确定依赖关系; import, export 不能放在块级作用于或条件语句中。
// 1. 命名导出
export const name = 'value'
export function method() {}
// 2. 默认导出
export default function() {}
// 3. 命名导入
import { name, method } from './module.js'
// 4. 默认导入
import defaultMethod from './module.js'
// 5. 混合导入
import defaultMethod, { name, method } from './module.js'5.3 加载机制 
ES6 Modules 和 CommonJS 的加载机制一样,对于相同的 js 文件,会保存静态属性。 但 ES6 Modules 的加载机制是提前加载并执行模块文件,ES6 模块在编译时就能确定模块的依赖关系,而 CommonJS 是同步的,需要等到运行时才能确定依赖关系。
1. 构造阶段
      ↓
2. 实例化阶段
      ↓
3. 求值阶段
      ↓
4. 所有模块加载完成
      ↓
5. 执行模块代码5.4 与 CommonJS 的主要区别 
加载时机:
- CommonJS:同步加载
 - ES Modules:异步加载
 
导入导出:
javascript// CommonJS const module = require('./module') module.exports = { /* ... */ } // ES Modules import module from './module' export const value = { /* ... */ }模块对象:
- CommonJS:值拷贝
 - ES Modules:引用绑定
 
循环依赖:
javascript// ES Modules 的循环依赖处理更优雅 // a.js import { b } from './b.js' export const a = 'a' // b.js import { a } from './a.js' export const b = 'b'
5.5 ES Modules 的优势 
静态分析:
- 支持树摇(tree-shaking)
 - 编译时优化
 - 更好的代码提示
 
实时绑定:
javascript// module.js export let count = 0 export function increment() { count++ } // main.js import { count, increment } from './module.js' console.log(count) // 0 increment() console.log(count) // 1严格模式:
- 自动启用严格模式
 - 更安全的代码执行环境
 
5.6 浏览器支持 
<!-- 1. 基本使用 -->
<script type="module" src="main.js"></script>
<!-- 2. 内联模块 -->
<script type="module">
    import { method } from './module.js'
</script>
<!-- 3. 降级处理 -->
<script nomodule src="fallback.js"></script>5.7 动态导入 
// 1. 条件导入
if (condition) {
    const module = await import('./module.js')
}
// 2. 按需导入
button.onclick = async () => {
    const { method } = await import('./module.js')
    method()
}- import 被导入的模块会保存静态属性,运行在严格模式;而 require 被导入的模块会保存动态属性,运行在非严格模式。
 - import 被导入的变量都是只读的,不能直接赋值;而 require 被导入的变量是可读可写的。
 - import 被导入得到变量是与原变量绑定/关联的,而 require 被导入得到变量是与原变量不绑定/不关联的。
 
import 功能
- 动态加载
 
 if(condition){
  import('./module.js').then(module => {
      console.log(module)
  })
 }- 懒加载
 
[
 {
      path: 'home',
      name: '首页',
      component: ()=> import('./home') ,
 },
]import() 这种加载效果,可以很轻松的实现代码分割。但也要避免一次性加载大量 js 文件,造成首次加载白屏时间过长的情况。
5.8 Tree-Shaking 
Tree-Shaking 是一种优化技术,用于移除未使用的代码,如一些被 import 但其实没有被使用到的代码。在 ES6 Modules 中,Tree-Shaking 可以有效减少打包后的代码体积。
6. CommonJs 和 Es Module 总结 
CommonJS 的特点总结:
运行时加载: CommonJS 是在代码运行时才确定模块的依赖关系。
// 你点菜时才知道需要哪些食材。导出方式简单: 只能导出一个对象(exports)。 // 就像一个包裹,所有要导出的东西都要放在这个包裹里。
灵活加载: 可以根据条件来决定加载哪个模块,而且会缓存加载过的模块避免重复加载。
// 就像去超市买东西,买过的东西会记在账上。同步执行: 加载模块时会暂停等待,直到模块加载完成。
// 就像排队买票,前面的人没买完,后面的人只能等着。
ES Module 的特点总结:
编译时加载: 在代码编译阶段就确定模块的依赖关系。 // 就像提前列好购物清单。
动态引用: 导入的值和原值是绑定的,原值改变时导入的值也会改变。
// 就像连体婴儿,一个动另一个也会动。导出方式灵活: 可以导出多个独立的值,可以按需导入导出。
// 就像从抽屉里可以单独拿东西,不用整个抽屉都搬走。更好的优化: 支持删除未使用的代码(Tree Shaking)和代码分割(Code Splitting)。 // 就像整理衣柜,把不穿的衣服扔掉,把衣服按季节分类。
严格模式: 默认在严格模式下运行,可以避免一些常见错误。
// 就像开车系安全带,更安全。