【Node.js】Common JS 和 ES Module 对于导出值的探究

CommonJS

在 CommonJS 模块中,模块的输出是对象引用的拷贝 。这意味着,如果导出的对象在模块内发生了修改,其他地方通过 require 导入的内容也会反映这些更改。

以下是详细说明:

  1. 模块缓存

    • CommonJS 模块在首次被 require 时,模块的代码会执行,导出对象会被创建并存储在缓存中。
    • 后续对该模块的 require 调用将返回缓存中的导出对象。
  2. 引用行为

    • 如果模块导出的是一个对象或数组,外部对该导入对象的修改会影响其他地方导入的对象,因为它们引用的是同一个内存地址。

示例:

moduleA.js:

javascript 复制代码
const data = { count: 1 };

function increment() {
  data.count++;
}

module.exports = { data, increment };

main.js:

javascript 复制代码
const moduleA = require('./moduleA');
console.log(moduleA.data.count); // 输出: 1

moduleA.increment();
console.log(moduleA.data.count); // 输出: 2

const anotherImport = require('./moduleA');
console.log(anotherImport.data.count); // 输出: 2

解释

  • module.exports 是一个对象的引用。
  • require('./moduleA') 返回缓存中相同的引用。
  • 修改 moduleA.data 会反映在所有导入中。

注意:

  • 如果直接导出原始值(如数字、字符串等),这将是一个值的拷贝,因为原始值是不可变的。例如:

moduleB.js:

javascript 复制代码
let count = 1;

function increment() {
  count++;
}

module.exports = { count, increment };

main.js:

javascript 复制代码
const moduleB = require('./moduleB');
console.log(moduleB.count); // 输出: 1

moduleB.increment();
console.log(moduleB.count); // 输出: 1(值的拷贝,不会自动更新)

总结

  • CommonJS 模块导出的对象是引用的拷贝。
  • 如果导出的是原始值,则导入后是值的拷贝,不会随着模块内的变化自动更新。

ES Module

在 ES Modules(ESM)中,模块的导出是通过引用的方式共享的。这意味着:

  1. 导出的值是动态绑定的

    • 导入的内容会实时反映模块中导出的最新状态(如果导出的值是对象或可变变量)。
    • ES Modules 会确保模块导出和导入的值保持同步。
  2. 与 CommonJS 的区别

    • CommonJS 在模块加载时会将导出的内容生成一次并缓存(静态快照,原始值不会同步更新)。
    • ESM 的导出是动态绑定的,即使导出的是原始值,更新模块内的值后,导入的地方也会反映出变化。

1. 对象引用

如果导出的是对象,导入的地方和模块内部共享同一个引用。

moduleA.mjs:

javascript 复制代码
export const data = { count: 1 };

export function increment() {
  data.count++;
}

main.mjs:

javascript 复制代码
import { data, increment } from './moduleA.mjs';

console.log(data.count); // 输出: 1

increment();
console.log(data.count); // 输出: 2

解释

  • data 是通过引用共享的,模块内部修改会直接反映到导入的地方。

2. 原始值(动态绑定)

如果导出的是原始值(如数字、字符串),模块内部更新的值会同步到导入的地方。

moduleB.mjs:

javascript 复制代码
export let count = 1;

export function increment() {
  count++;
}

main.mjs:

javascript 复制代码
import { count, increment } from './moduleB.mjs';

console.log(count); // 输出: 1

increment();
console.log(count); // 输出: 2

解释

  • count 是动态绑定的,尽管它是原始值,模块内部的更新会反映到导入的地方。

ESM 的特点:

  1. 静态结构

    • ES Modules 是在编译时解析的,importexport 必须位于顶层。
    • 无法在运行时动态导入或导出(但可以使用 import() 动态加载模块)。
  2. 实时绑定

    • ESM 的导出是动态绑定的,导入的值会随模块内的更新而更新。
  3. 单例模式

    • ES Modules 是单例的,多次导入同一个模块时,得到的都是同一个模块实例。

对比总结

特性 CommonJS ES Modules
导出机制 值的拷贝(原始值),引用拷贝(对象) 动态绑定
导入时机 运行时(动态加载) 编译时(静态加载)
更新机制 静态快照 实时更新
模块缓存
用法 require() import/export

因此,ESM 更适合处理动态绑定和模块间实时同步的场景。

其他区别

1. 模块加载时机

  • CommonJS :模块加载是 同步 的,模块加载时会立即执行,并返回模块的导出内容。因此,require 语句会阻塞,直到模块完全加载并执行完成。
  • ESM :模块加载是 异步 的,在浏览器和 Node.js 中,ESM 模块在加载时不会阻塞,尤其是在浏览器环境下,模块加载会是异步的。

2. 文件扩展名

  • CommonJS :支持 .js, .json, 和 .node 扩展名的文件。模块导入时,如果没有扩展名,Node.js 会自动尝试加载 .js.json.node 文件。
  • ESM :在 Node.js 环境下,ESM 强制要求指定扩展名,除非使用 .js 文件且该文件是 ES Module(在 package.json 中指定 "type": "module")。这意味着必须明确指定扩展名,如 import x from './module.mjs'import x from './module.js'

3. 模块的加载方式

  • CommonJS :在执行 require 时,模块被加载并执行一次,之后会缓存模块的导出内容。模块执行的顺序是按调用 require 的顺序同步执行。
  • ESM :ESM 模块的加载是 按需 加载的,ESM 模块会被动态解析并可能会被异步加载。ESM 模块会被延迟执行,且加载过程支持静态分析,允许更强大的工具进行优化和死代码删除。

4. importexport 语法

  • CommonJS

    • 导入:const foo = require('foo');
    • 导出:module.exports = foo;exports.foo = foo;
  • ESM

    • 导入:import foo from 'foo';import { bar } from 'foo';
    • 导出:export default foo;export { foo };

    ES Modules 使用静态的 importexport 语法,在编译时就可以解析出模块的依赖关系,而 CommonJS 使用动态的 requiremodule.exports,只能在运行时解析。

5. this 的行为

  • CommonJS :在模块中,this 默认指向 module.exports。这意味着如果你没有显式地导出模块,this 将指向一个空对象({})。

    javascript 复制代码
    // CommonJS
    console.log(this); // 输出: {}
    this.foo = 'bar';
    console.log(module.exports); // 输出: { foo: 'bar' }
  • ESMthis 在 ESM 模块中并不会指向 exportsmodule.exports,它指向 undefined。在 ESM 中,exportimport 被静态解析,this 并不是用于模块导出的机制。

    javascript 复制代码
    // ESM
    console.log(this); // 输出: undefined

6. 异步性

  • CommonJS:模块加载是同步的,因此适合于服务器端的环境,尤其是在 Node.js 中,文件读取通常是同步的。

  • ESM :ESM 支持异步加载模块,尤其是在浏览器中,ESM 是按需加载的,可以延迟模块的加载,提高性能。对于 Node.js,也提供了 import() 异步加载的能力。

    javascript 复制代码
    // 使用动态导入
    import('./module.js').then(module => {
      console.log(module);
    });

7. 循环依赖处理

  • CommonJS :处理循环依赖时,CommonJS 会在模块第一次加载时返回模块的当前状态,后续 require 调用会返回这个已经部分执行的模块。这意味着如果一个模块还未完全执行,其他模块就可以访问到它的部分导出。

  • ESM :ESM 通过引入 动态绑定 的方式来处理循环依赖。如果模块 A 导入模块 B,而模块 B 又导入模块 A,模块 A 和 B 中的导出会是动态的绑定关系,始终反映最新的值。

8. 命名空间导出

  • CommonJS :导出的内容是一个对象,可以直接添加到 module.exportsexports 上,这通常意味着你可以在导出时使用任意结构(如对象、数组、函数等)。

    javascript 复制代码
    // CommonJS 导出
    module.exports = { foo: 'bar', baz: 'qux' };
  • ESM:ESM 使用静态导出和导入的机制,且导入时会自动生成一个"命名空间"对象。这意味着你可以选择性地导入模块的部分功能。

    javascript 复制代码
    // ESM 导出
    export const foo = 'bar';
    export const baz = 'qux';

9. requireimport 的行为

  • CommonJSrequire 是一个动态函数,可以在任何地方使用,因此可以在条件语句、循环中动态加载模块。

    javascript 复制代码
    if (condition) {
      const module = require('./module');
    }
  • ESMimport 是静态的,必须在模块的顶部使用,并且不能在条件语句、循环中动态加载。import 的静态特性使得打包工具(如 Webpack)能够优化模块的加载顺序和去除死代码。

    javascript 复制代码
    // ESM 只能在顶部使用
    import { foo } from './module';

10. 浏览器支持

  • CommonJS:原生不支持浏览器,通常需要通过打包工具(如 Webpack、Browserify)进行转换。

  • ESM :ESM 从一开始就设计为浏览器支持的标准。现代浏览器原生支持 <script type="module"> 标签,可以直接加载 ESM 模块。Node.js 在 v12+ 也原生支持 ESM。

11. 模块作用域

  • CommonJS:模块是封装在一个函数中,模块内部的变量和函数默认不污染全局作用域。

  • ESM:ESM 的模块作用域也类似,每个模块都有自己的作用域,且不污染全局作用域。ESM 具有更强的静态分析能力和模块系统支持。


总结

特性 CommonJS ES Modules
加载方式 同步加载 异步加载
语法 require(), module.exports import, export
模块缓存 模块首次加载时缓存 模块首次加载时缓存
模块作用域 模块内有自己的作用域,this 默认指向 module.exports 模块内有自己的作用域,this 指向 undefined
循环依赖 部分执行的模块对象 动态绑定,解决循环依赖
扩展名 支持 .js, .json, .node 强制需要扩展名(如 .js, .mjs
适用场景 主要用于服务器端,Node.js 适用于浏览器和 Node.js,支持异步加载
相关推荐
烂蜻蜓16 分钟前
前端已死?什么是前端
开发语言·前端·javascript·vue.js·uni-app
Rowrey1 小时前
react+typescript,初始化与项目配置
javascript·react.js·typescript
祈澈菇凉6 小时前
Webpack的基本功能有哪些
前端·javascript·vue.js
小纯洁w6 小时前
Webpack 的 require.context 和 Vite 的 import.meta.glob 的详细介绍和使用
前端·webpack·node.js
记得早睡~7 小时前
leetcode150-逆波兰表达式求值
javascript·算法·leetcode
Elastic 中国社区官方博客7 小时前
Elasticsearch Open Inference API 增加了对 Jina AI 嵌入和 Rerank 模型的支持
大数据·人工智能·elasticsearch·搜索引擎·ai·全文检索·jina
隔壁老王1567 小时前
mysql实时同步到es
数据库·mysql·elasticsearch
庸俗今天不摸鱼7 小时前
Canvas进阶-4、边界检测(流光,鼠标拖尾)
开发语言·前端·javascript·计算机外设
熬夜不洗澡7 小时前
Node.js中不支持require和import两种导入模块的混用
node.js
bubusa~>_<7 小时前
解决npm install 出现error,比如:ERR_SSL_CIPHER_OPERATION_FAILED
前端·npm·node.js