请问: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“。 点击右边的图片试试~ 两个非文本元素——图片与超链接。 从现在开始࿰…...

地震勘探——干扰波识别、井中地震时距曲线特点
目录 干扰波识别反射波地震勘探的干扰波 井中地震时距曲线特点 干扰波识别 有效波:可以用来解决所提出的地质任务的波;干扰波:所有妨碍辨认、追踪有效波的其他波。 地震勘探中,有效波和干扰波是相对的。例如,在反射波…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...
ES6从入门到精通:前言
ES6简介 ES6(ECMAScript 2015)是JavaScript语言的重大更新,引入了许多新特性,包括语法糖、新数据类型、模块化支持等,显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...

Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
LCTF液晶可调谐滤波器在多光谱相机捕捉无人机目标检测中的作用
中达瑞和自2005年成立以来,一直在光谱成像领域深度钻研和发展,始终致力于研发高性能、高可靠性的光谱成像相机,为科研院校提供更优的产品和服务。在《低空背景下无人机目标的光谱特征研究及目标检测应用》这篇论文中提到中达瑞和 LCTF 作为多…...

Elastic 获得 AWS 教育 ISV 合作伙伴资质,进一步增强教育解决方案产品组合
作者:来自 Elastic Udayasimha Theepireddy (Uday), Brian Bergholm, Marianna Jonsdottir 通过搜索 AI 和云创新推动教育领域的数字化转型。 我们非常高兴地宣布,Elastic 已获得 AWS 教育 ISV 合作伙伴资质。这一重要认证表明,Elastic 作为 …...

Neko虚拟浏览器远程协作方案:Docker+内网穿透技术部署实践
前言:本文将向开发者介绍一款创新性协作工具——Neko虚拟浏览器。在数字化协作场景中,跨地域的团队常需面对实时共享屏幕、协同编辑文档等需求。通过本指南,你将掌握在Ubuntu系统中使用容器化技术部署该工具的具体方案,并结合内网…...