请问:ESModule 与 CommonJS 的异同点是什么?
前言
本篇文章不会介绍模块的详细用法,因为核心是重新认识和理解模块的本质内容是什么,直奔主题,下面先给出最后结论,接下来在逐个进行分析。
ECMAScript Module 和 CommonJS 的相同点:
- 都拥有自己的缓存机制,即 多次加载 同一个模块,该模块内容 只会执行一次
CommonJS模块内容执行完成后,会生成 Module 对象,同时这个对象会被缓存到require.cache对象中ECMAScript模块拥有自己的缓存机制,并使得模块中的变量和该模块进行锁定,保证外部模块可以访问内部变量的最新值
- 可对于输出的接口进行修改
ECMAScript模块输出的是一个只读引用,相当于通过const进行声明,意味着不能修改输出接口的引用,但可以修改引用中的内容CommonJS模块默认没有上述的限制,但一般接收模块输出接口时大多都会使用const进行声明,此时它们的表现将一致,但如果使用类似let a = require('./a.js')的方式加载模块,那么对变量a的引用可以随意更改
ECMAScript Module 和 CommonJS 的差异:
- 加载时机不同
ECMAScript模块是 编译时输出接口CommonJS模块是 运行时加载
- 加载方式不同
ECMAScript模块的import命令是 异步加载,有一个独立的模块依赖的解析阶段CommonJS模块的require()是 同步加载模块
- 输出结果不同
ECMAScript模块输出的是 值的引用CommonJS模块输出的是一个 值的浅拷贝
- 缓存方式不同
CommonJS模块通过require.cache来对值进行缓存ECMAScript模块拥有自己的缓存机制
- 处理循环加载的方式不同
CommonJS模块发生 循环加载 时,只输出已经执行部分,未执行部分不会输出ECMAScript模块发生 循环加载 时,默认 循环加载 模块内部已经执行完毕,对输出接口是否能使用成功需要开发者自己保证
接下来,先简单了解下 Node.js 的模块加载方法是什么?
Node.js 的模块加载方法
Node.js 有两个模块系统:
CommonJS 模块,简称 CJSECMAScript 模块,即 ES6 模块,简称 ESM
CommonJS 模块
CommonJS 模块是为 Node.js 打包 JavaScript 代码的原始方式,模块使用require()和module.exports 语句定义。
默认情况下,Node.js 会将以下内容视为 CommonJS 模块:
- 扩展名为
.cjs的文件 - 当最近的父
package.json文件中 包含 顶层字段"type: "commonjs"或 不包含 顶层字段"type"时,则应用于扩展名为.js的文件 - 当最近的父
package.json文件包包含顶层字段"type": "module"时,对于扩展名不是.mjs、.cjs、.json、.node、或.js的文件,只有当它们通过require被加载时才会被认为是 CommonJS 模块,且不是用作程序的命令行入口点
加载原理
CommonJS 的一个模块,就是一个脚本文件,require 命令 第一次加载 脚本时,会 执行整个脚本,然后在内存中 生成一个 Module 对象。
详细信息可以观察以下示例代码和输出结果:
// a.js
var name = "name in a.js"
module.exports = {name
}
console.log("module in a.js")
console.log(module)// index.js
const a = require("./a.js")
console.log('module a in index.js', a)
在终端通过 node index.js 执行后,得到结果如下:

在上图中,该对象的 id 属性是模块名,exports 属性是模块输出的各个接口,loaded 属性是一个布尔值,表示该模块的脚本是否执行完毕,children 属性是当前模块依赖的其他模块集合,其他略过。
模块缓存
CommonJS 模块无论加载多少次,都只会在 第一次加载时运行一次,并生成上面的 Module 对象,以后再加载相同模块,就返回第一次运行的结果,即 Module 对象,除非手动清除系统缓存。
可以通过输出
require.cache查看当前模块的缓存内容
仍然通过示例代码和输出结果来观察:
// a.js
const a1 = require("./a.js")
console.log('first load a.js', a1)const a2 = require("./a.js")
console.log('second load a.js', a2)console.log('a1 === a2 =>', a1 === a2)// index.js
var name = "name in a.js"
console.log("loading a.js")
module.exports = {name
}

通过上图可以看出,多次加载同一个模块,模块内容只会执行一次,而且得到都是第一次生成的 Module 对象,其中包含了模块输出的各个接口。
输出的是值的拷贝
CommonJS 模块输出的是值的拷贝,即一旦输出一个值,模块内部的变化就影响不到这个值。
示例代码和输出结果如下:
// index.js
const a = require('./a.js');
console.log('before add in index.js,a.count = ', a.count);
a.add();
console.log('after add in index.js,a.count = ', a.count);// a.js
let count = 0;function add() {count++;console.log('add call in a.js,count = ', count);
}module.exports = {count,add
}

模块的循环加载
CommonJS 模块的重要特性是 加载时执行,即脚本代码在进行 require 时,就会全部执行。一旦出现某个模块被 “循环加载”,只输出已经执行部分,未执行部分不会输出。
下面通过 Node 官方文档 循环部分相关的例子来进行演示:
// main.js
console.log('【【【 main starting 】】】');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
console.log('【【【 main done 】】】');// a.js
console.log('==== a starting ====');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('==== a done ====');// b.js
console.log('<<<< b starting >>>>');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('<<<< b done >>>>');
其中,a.js 模块和 b.js 模块会相互加载,此时就会产生 “循环加载”,输出结果如:

核心步骤分析如下:
main.js先执行到const a = require('./a.js');时,进入a模块并执行a.js中第二行为模块添加了done属性,即exports.done = false;,接着执行const b = require('./b.js');时,进入b模块并执行b.js中第二行为模块添加了done属性,即exports.done = false;,接着执行const a = require('./a.js');此时 发生循环,因此回到a模块中,但此时发现a模块以及执行过了,因此直接使用是上次的缓存 Module 对象,此时b模块中访问a.done就是false,因为a模块中没有执行完,即 只输出已经执行部分b模块执行到exports.done = false;处,核心步骤已完成并输出,会返回a模块中把 未执行完的部分继续执行完成,此时exports.done = false;
main.js后执行到const b = require('./b.js');时,发现b模块已经执行过了,于是在这拿到的就是第一次执行缓存的 Module 对象,接着在main.js访问a.done和b.done的值就都是true
ECMAScript 模块
ECMAScript 模块 是来打包 JavaScript 代码以供重用的 官方标准格式,模块使用 import 和 export 语句定义。
从 Node.js v13.2 版本开始,Node.js 默认打开了对 ECMAScript 模块 的支持
加载原理
ECMAScript 模块的运行机制与 CommonJS 不一样,JS 引擎 在对脚本进行 静态分析 时,只要遇到模块加载命令 import ,就会生成一个 只读引用,等到脚本 真正执行 时,再根据这个 只读引用,去被加载的模块中 取值。
ECMAScript 模块是 静态分析 阶段生成的 只读引用,因此不好演示具体示例,但可通过下面的例子来验证 只读引用,即相当于通过 const 关键字进行了声明。
// a.mjs
let count = 0export {count
}// index.mjs
import {count, add} from './a.mjs'console.log('count = ', count)
count = 1
console.log('count = ', count)

模块缓存
ECMAScript 模块 没有使用 CommonJS 模块的 require.cache 缓存方式,因为 ECMAScript 模块加载器有自己独立的缓存。
代码示例和输出结果如下:
// a.mjs
console.log('load a.mjs')// index.mjs
import './a.mjs'
import './a.mjs'

输出的是值的引用
ECMAScript 模块输出的是值的引用,即 ECMAScript 模块是 动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
示例代码和输出结果如下:
// a.mjs
import {count, add} from './a.mjs'console.log('before add,count = ', count)
add()
console.log('after add,count = ', count)// index.mjs
let count = 0let add = () => {count++console.log('add call in a.mjs,count = ', count)
}export {count,add
}

模块的循环加载
ECMAScript 模块处理 “循环加载” 与 CommonJS 模块有本质的不同。
ES6 模块 是动态引用,如果使用 import 从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个 指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
示例代码和输出结果如下:
// index.mjs
import './a.mjs'// a.mjs
import {bar} from './b.mjs';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';// b.mjs
import {foo} from './a.mjs';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

详细步骤分析如下:
- 在
index.mjs中通过import './a.mjs'执行a模块 - 进入
a.mjs模块并开始执行,引擎发现它加载b.mjs,因此会优先执行b.mjs - 进入
b.mjs模块并开始执行,引擎发现b又需要加载a.mjs,并接收了a模块中输出的foo接口,但此时并不会去执行a.mjs,而是认为这个接口已经存在了,于是继续往下执行,当执行到console.log(foo)处,才发现这个接口根本没定义,因此会产生错误 - 如果
b.mjs中没有发生异常,那么在执行完b模块后,会再返回去执行a.mjs
循环加载报错的解决方案
本质原因就是发生 “循环加载” 时,ECMAScript 模块会默认循环模块内容已经执行完成,但是实际是没有执行完成,导致在引用循环模块中的接口时,报错本质上也可以认为是 ES6 中的 暂时性死区 引发的报错。
因此,我们可以通过将对应的 export let foo = 'foo'; 的声明方式换为:
var的声明方式,如:export var foo = 'foo';,- 或将
foo变量换成 函数声明,如export function foo(){ return 'bar'};
就可以解决问题,因为它们都具有 “变量提升”,因此,即便 a 模块没有被执行完,也可以在 b 模块中正常进行访问,但是要注意使用场景。

不同模块的相互加载
CommonJS 模块加载 ECMAScript 模块
CommonJS 的 require() 命令不能加载 ECMAScript 模块,这会产生报错,因此只能使用 import() 这个方法加载。
require() 不支持 ECMAScript 模块的一个原因是,require() 是同步加载,而 ECMAScript 模块内部可以使用顶层 await 命令,导致无法被同步加载。
示例代码和输出结果如下:
// a.mjs
let name = 'a.mjs'
export default name// index.js
(async () => {let a = await import('./a.mjs');console.log(a);
})();

ECMAScript 模块加载 CommonJS 模块
ECMAScript 模块的 import 命令可以加载 CommonJS 模块,但是 只能整体加载,不能只加载单一的输出项。
示例代码和输出结果如下:
// a.js
let name = 'a.js'module.exports = {name
}// index.mjs
import a from './a.js'
console.log(a)

这是因为 ECMAScript 模块需要支持 静态代码分析,而 CommonJS 模块的输出接口的 module.exports 是一个对象,无法被静态分析,所以只能整体加载。
同时支持两种格式的模块
一个模块同时要支持 CommonJS 和 ECMAScript 两种格式,那么需要进行判断:
- 如果原始模块是 ECMAScript 格式,那么需要给出一个整体输出接口,比如
export default obj,使得 CommonJS 可以用import()进行加载 - 如果原始模块是 CommonJS 格式,那么可以加一个包装层,即先整体输入 CommonJS 模块,然后再根据 ECMAScript 格式按需要输出具名接口
import cjsModule from '../index.js'; // CommonJS 格式 export const foo = cjsModule.foo; // ECMAScript 格式 - 另一种做法是通过在
package.json文件中的exports字段,指明两种格式 模块各自的 加载入口,下面代码指定require()和import,加载该模块时会自动切换到不同的入口文件"exports":{"require": "./index.js","import": "./esm/wrapper.js" }
参考资源
- Module 的加载实现 - 阮一峰
- Node.js 官方文档 —— CommonJS 模块
- Node.js 官方文档 —— ECMAScript 模块
相关文章:
请问:ESModule 与 CommonJS 的异同点是什么?
前言 本篇文章不会介绍模块的详细用法,因为核心是重新认识和理解模块的本质内容是什么,直奔主题,下面先给出最后结论,接下来在逐个进行分析。 ECMAScript Module 和 CommonJS 的相同点: 都拥有自己的缓存机制&#…...
【数据结构与算法】力扣 59. 螺旋矩阵 II
题目描述 给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1: 输入: n 3 输出: [[1,2,3],[8,9,4],[7,6,5]]示例 2: 输入:…...
HarmonyOS Next模拟器异常问题及解决方法
1、问题1:Failed to get the device apiVersion. 解决方法:关闭模拟器清除用户数据重启...
求最大公约数(c语言)
先看题👇 我这里介绍的方法:辗转相除法: 最大公约数: 最大公约数是指同时能整除俩个或更多整数的最大正整数。 欧几里得算法就是求最大公约数的算法 求最大公约数涉及到一个数学原理的转换: 俩个数的最大公约数等于其中一个数和…...
Android Camera2在textureView中的预览和拍照
Camera2预览和拍照 1、Camera2相机模型2、Camera2的重要类3、Camera2调用流程4、Camera2调用实现 1)定义TextureView作为预览界面2)设置相机参数3)开启相机4)开启相机预览5)实现PreviewCallback6)拍照 1、Camera2相机模型 解释上诉示意图,假如想要同时拍摄两张不同…...
Redis的缓存问题
缓存雪崩 定义:缓存雪崩是指在某个时间段内,缓存中的大量数据同时失效或者大量的请求集中到某一个时间点发生,导致数据库压力骤增,甚至引起服务崩溃的现象。 原因:通常是由于缓存中的大量数据同时过期或者大量的请求集…...
C语言小游戏--猜数字
游戏过程: 由电脑随机在某个范围内生成一个数字,玩家猜数字并且输入,电脑判断是否正确,正确则游戏结束,错误则给出提示,直到玩家所给的答案正确为止 思路分析: 1.生成随机数 2.玩家可以多次…...
代理IP在爬虫中的作用是什么?
在爬虫中,代理IP的主要作用包括以下几个方面: 防止IP被封禁:每个网站都有反爬机制,会记录并封禁同一个IP地址的频繁请求。使用代理IP可以让爬虫更换源头,减少被目标网站识别为恶意爬虫的风险。 提高抓取效率ÿ…...
卡尔曼讲解与各种典型进阶MATLAB编程(专栏目录,持续更新……)
专栏链接:https://blog.csdn.net/callmeup/category_12574912.html 文章目录 专栏介绍重点文章卡尔曼滤波的原理卡尔曼滤波的例程 进阶MATLAB编程后续更新 专栏介绍 本专栏旨在深入探讨卡尔曼滤波及其在各类应用中的实现,尤其是通过MATLAB编程进行的典…...
Java项目-基于Springboot的智慧养老平台项目(源码+文档).zip
作者:计算机学长阿伟 开发技术:SpringBoot、SSM、Vue、MySQL、ElementUI等,“文末源码”。 开发运行环境 开发语言:Java数据库:MySQL技术:SpringBoot、SpringClud、Vue、Mybaits Plus、ELementUI工具&…...
如何测试IP速度?
了解代理的连接速度是否快速是确保网络使用效率和体验的关键因素之一。本文来为大家如何有效地评估和测试代理IP的连接速度,以及一些实用的方法和工具,帮助用户做出明智的选择和决策。 一、如何评估代理IP的连接速度 1. 使用在线速度测试工具 为了快速…...
IDEA使用Alibaba Cloud Toolkit插件自动化部署jar包
一、下载插件 二、添加服务器主机 三、填写自己服务器配置 四、添加配置 五、配置说明 六、选择maven打包模块 七、maven打包后的jar包位置配一下 八、点击运行发现成功...
FFMPEG录屏(19)--- 枚举Windows下的屏幕列表,并获取名称、缩略图
在Windows下枚举显示器列表并获取名称、缩略图 在Windows系统中,枚举显示器列表并获取它们的名称和缩略图是一个常见的需求。本文将详细介绍如何实现这一功能,涉及到的主要技术包括Windows API和C编程。 获取显示器信息 首先,我们需要一个…...
【python】NumPy(三):文件读写
目录 前言 NumPy 常见IO函数 save()和load() savez() loadtxt()和savetxt() 练习 前言 在数据分析中,我们经常需要从文件中读取数据或者将数据写入文件,常见的文件格式有:文本文件txt、CSV格式文件(用逗号分隔ÿ…...
硬件产品经理的开店冒险之旅(下篇)
缘起:自己为何想要去寻找职业第二曲线 承接上篇的内容,一名工作13年的普通硬件产品经理将尝试探索第二职业曲线。根本原因不是出于什么高大上的人生追求或者什么职业理想主义,就是限于目前的整体就业形式到了40岁的IT从业人员基本不可能在岗…...
基于GeoScene Pro的开源数据治理与二维制图规范化处理智能工具箱
内容导读 本文描述的是一个基于GeoScene Pro4.0/ArcGIS3.1 Pro平台的开源数据治理与二维制图规范化处理智能工具箱(免费试用,文末有获取方式),旨在解决GIS应用中数据转换、检查、治理和制图数据规范化处理方面的问题。 工具箱结合了Geoscene/ArcGIS Pr…...
CSS 设置网页的背景图片
背景 最近正好在写一个个人博客网站“小石潭记”,需要一张有水,有鱼的图片。正好玩原神遇到了类似场景,于是截图保存,添加到网站里面。以下是效果图: css 写个class,加到整个网页的body上 .bodyBg {ba…...
如何使用DockerSpy检测你的Docker镜像是否安全
关于DockerSpy DockerSpy是一款针对Docker镜像的敏感信息检测与安全审计工具,该工具可以帮助广大研究人员在Docker Hub上检测和搜索自己镜像的安全问题,并识别潜在的泄漏内容,例如身份验证密钥等敏感信息。 功能介绍 1、安全审计:…...
数据结构练习题4(链表)
1两两交换链表中的节点 给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。 示例 1: 输入:head [1,2,3,4]…...
【前端】如何制作自己的网站(7)
以下内容接上文。 结合图片的超链接 将img元素作为内容,放在a元素中。即可为图片添加一个超链接。 例如右边的代码,点击头像就会打开“aboutme.html“。 点击右边的图片试试~ 两个非文本元素——图片与超链接。 从现在开始࿰…...
(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...
ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
uniapp 小程序 学习(一)
利用Hbuilder 创建项目 运行到内置浏览器看效果 下载微信小程序 安装到Hbuilder 下载地址 :开发者工具默认安装 设置服务端口号 在Hbuilder中设置微信小程序 配置 找到运行设置,将微信开发者工具放入到Hbuilder中, 打开后出现 如下 bug 解…...
HTML前端开发:JavaScript 获取元素方法详解
作为前端开发者,高效获取 DOM 元素是必备技能。以下是 JS 中核心的获取元素方法,分为两大系列: 一、getElementBy... 系列 传统方法,直接通过 DOM 接口访问,返回动态集合(元素变化会实时更新)。…...
LangChain 中的文档加载器(Loader)与文本切分器(Splitter)详解《二》
🧠 LangChain 中 TextSplitter 的使用详解:从基础到进阶(附代码) 一、前言 在处理大规模文本数据时,特别是在构建知识库或进行大模型训练与推理时,文本切分(Text Splitting) 是一个…...
CppCon 2015 学习:REFLECTION TECHNIQUES IN C++
关于 Reflection(反射) 这个概念,总结一下: Reflection(反射)是什么? 反射是对类型的自我检查能力(Introspection) 可以查看类的成员变量、成员函数等信息。反射允许枚…...
SQL注入篇-sqlmap的配置和使用
在之前的皮卡丘靶场第五期SQL注入的内容中我们谈到了sqlmap,但是由于很多朋友看不了解命令行格式,所以是纯手动获取数据库信息的 接下来我们就用sqlmap来进行皮卡丘靶场的sql注入学习,链接:https://wwhc.lanzoue.com/ifJY32ybh6vc…...
【版本控制】GitHub Desktop 入门教程与开源协作全流程解析
目录 0 引言1 GitHub Desktop 入门教程1.1 安装与基础配置1.2 核心功能使用指南仓库管理日常开发流程分支管理 2 GitHub 开源协作流程详解2.1 Fork & Pull Request 模型2.2 完整协作流程步骤步骤 1: Fork(创建个人副本)步骤 2: Clone(克隆…...
