请问:ESModule 与 CommonJS 的异同点是什么?

news2024/10/18 10:42:50

前言

本篇文章不会介绍模块的详细用法,因为核心是重新认识和理解模块的本质内容是什么,直奔主题,下面先给出最后结论,接下来在逐个进行分析。

ECMAScript ModuleCommonJS 的相同点:

  • 都拥有自己的缓存机制,即 多次加载 同一个模块,该模块内容 只会执行一次
    • CommonJS 模块内容执行完成后,会生成 Module 对象,同时这个对象会被缓存到 require.cache 对象中
    • ECMAScript 模块拥有自己的缓存机制,并使得模块中的变量和该模块进行锁定,保证外部模块可以访问内部变量的最新值
  • 可对于输出的接口进行修改
    • ECMAScript 模块输出的是一个只读引用,相当于通过 const 进行声明,意味着不能修改输出接口的引用,但可以修改引用中的内容
    • CommonJS 模块默认没有上述的限制,但一般接收模块输出接口时大多都会使用 const 进行声明,此时它们的表现将一致,但如果使用类似 let a = require('./a.js') 的方式加载模块,那么对变量 a 的引用可以随意更改

ECMAScript ModuleCommonJS 的差异:

  • 加载时机不同
    • ECMAScript 模块是 编译时输出接口
    • CommonJS 模块是 运行时加载
  • 加载方式不同
    • ECMAScript 模块的 import 命令是 异步加载,有一个独立的模块依赖的解析阶段
    • CommonJS 模块的 require()同步加载模块
  • 输出结果不同
    • ECMAScript 模块输出的是 值的引用
    • CommonJS 模块输出的是一个 值的浅拷贝
  • 缓存方式不同
    • CommonJS 模块通过 require.cache 来对值进行缓存
    • ECMAScript 模块拥有自己的缓存机制
  • 处理循环加载的方式不同
    • CommonJS 模块发生 循环加载 时,只输出已经执行部分未执行部分不会输出
    • ECMAScript 模块发生 循环加载 时,默认 循环加载 模块内部已经执行完毕,对输出接口是否能使用成功需要开发者自己保证

接下来,先简单了解下 Node.js 的模块加载方法是什么?

Node.js 的模块加载方法

Node.js 有两个模块系统:

  • CommonJS 模块,简称 CJS
  • ECMAScript 模块,即 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 模块会相互加载,此时就会产生 “循环加载”,输出结果如:

image.png

核心步骤分析如下:

  • 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.doneb.done 的值就都是 true

ECMAScript 模块

ECMAScript 模块 是来打包 JavaScript 代码以供重用的 官方标准格式,模块使用 importexport 语句定义。

Node.js v13.2 版本开始,Node.js 默认打开了对 ECMAScript 模块 的支持

加载原理

ECMAScript 模块的运行机制与 CommonJS 不一样,JS 引擎 在对脚本进行 静态分析 时,只要遇到模块加载命令 import ,就会生成一个 只读引用,等到脚本 真正执行 时,再根据这个 只读引用,去被加载的模块中 取值

ECMAScript 模块是 静态分析 阶段生成的 只读引用,因此不好演示具体示例,但可通过下面的例子来验证 只读引用,即相当于通过 const 关键字进行了声明。

// a.mjs
let count = 0

export {
  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 = 0

let 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 模块

CommonJSrequire() 命令不能加载 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 是一个对象,无法被静态分析,所以只能整体加载。

同时支持两种格式的模块

一个模块同时要支持 CommonJSECMAScript 两种格式,那么需要进行判断:

  • 如果原始模块是 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 模块

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2217715.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FreeRTOS - 软件定时器

在学习FreeRTOS过程中&#xff0c;结合韦东山-FreeRTOS手册和视频、野火-FreeRTOS内核实现与应用开发、及网上查找的其他资源&#xff0c;整理了该篇文章。如有内容理解不正确之处&#xff0c;欢迎大家指出&#xff0c;共同进步。 1. 软件定时器 软件定时器也可以完成两类事情…

安卓流式布局实现记录

效果图&#xff1a; 1、导入第三方控件 implementation com.google.android:flexbox:1.1.0 2、布局中使用 <com.google.android.flexbox.FlexboxLayoutandroid:id"id/baggageFl"android:layout_width"match_parent"android:layout_height"wrap_co…

在Linux操作系统上安装NVM教程——CentOS 8/VMware 17版本

目录 一、查看网络配置 二、配置阿里云镜像 三、下载NVM 四、给虚拟机共享本机文件&#xff08;补充&#xff09; 一、查看网络配置是否能上网 1.查看文件&#xff1a;cat /etc/sysconfig/network-scripts/ifcfg-ens160&#xff08;注意&#xff1a;ONBOOT"yes"…

Kibana可视化Dashboard如何基于字段是否包含某关键词进行过滤

kinana是一个功能强大、可对Elasticsearch数据进行可视化的开源工具。 我们在dashboard创建可视化时&#xff0c;有时需要将某个index里数据的某个字段根据是否包含某些特定关键词进行过滤&#xff0c;这个时候就可以用到lens里的filter功能很方便地进行操作。 如上图所示&…

汽车与航空领域的功能安全对比:ISO 26262-6 与 DO-178C 的差异浅析

ISO 26262-6 和 DO-178C &#xff08;航空系统与设备认证中的软件考量&#xff09;。是汽车和航空领域分别广泛应用的软件安全标准。它们的共同目标是确保系统软件可靠性&#xff0c;减少系统软件故障对生命安全的威胁&#xff0c;但在具体的软件安全方案和规范实施上存在明显的…

python的两个路径

xxx/python.exe&#xff08;解释器位置&#xff09; sdsd/xx/xx.py&#xff08;文件位置&#xff09; 在命令行中运行python的时候&#xff0c;命令行所在位置是os.getcwd()&#xff0c;bash是操作系统相关组件 假如脚本中执行fopen(a.txt)&#xff0c;这里的相对路径a.txt也…

【K8S系列】Kubernetes pod节点NotReady问题及解决方案详解【已解决】

Kubernetes 集群中的每个节点都是运行容器化应用的基础。当节点状态显示为 NotReady 时&#xff0c;意味着该节点无法正常工作&#xff0c;这可能会导致 Pod 无法调度&#xff0c;从而影响整个应用的可用性。本文将深入分析节点不健康的各种原因、详细的排查步骤以及有效的解决…

在CentOS系统下实现准实时SFTP上传指定目录下前2分钟的文件

在CentOS系统下实现准实时SFTP上传指定目录下前2分钟的文件 引言准备工作编写Shell脚本执行脚本定时执行脚本注意事项结论引言 在企业级的文件同步和备份场景中,经常需要将本地目录中最新生成的文件(如前2分钟内生成的文件)快速上传到远程服务器的指定目录。为了实现这一目…

【银河麒麟高级服务器操作系统-实例】集群存储文件系统异常,本地复现+详细分析+解决建议

了解更多银河麒麟操作系统全新产品&#xff0c;请点击访问 麒麟软件产品专区&#xff1a;https://product.kylinos.cn 开发者专区&#xff1a;https://developer.kylinos.cn 文档中心&#xff1a;https://documentkylinos.cn 服务器环境以及配置 【机型】物理机 TG225 B1 处…

XML\XXE漏洞基本原理

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文整理XXE漏洞的相应信息 XML与XXE漏洞 这个东西有许多叫法&#xff0c;XML漏洞与XXE漏洞差不多都是一个东西。 这个漏洞是出现在XMl上的&#xff0c;然后可以叫他XXE注入漏洞。 XML简介 XML是一种数据的传输…

5G NR:UE初始接入信令流程浅介

UE初始接入信令流程 流程说明 用户设备&#xff08;UE&#xff09;向gNB-DU发送RRCSetupRequest消息。gNB-DU 包含 RRC 消息&#xff0c;如果 UE 被接纳&#xff0c;则在 INITIAL UL RRC MESSAGE TRANSFER 消息中包括为 UE 分配的低层配置&#xff0c;并将其传输到 gNB-CU。IN…

测试说明

1.修改数据集 将 for_redistribution_files_only 文件夹下的 valid_data.csv 换成测试数据&#xff0c;文件名不要改变仍为valid_data.csv 2.加载镜像 在matlab-runtime-R2020a.tar所在路径下打开cmd&#xff0c;运行以下命令 docker load -i matlab-runtime-R2020a.tar 稍等…

jmeter中对于有中文内容的csv文件怎么保存

jmeter的功能很强大&#xff0c;但是细节处没把握好就得不到预期的结果。今天来讲讲有中文内容的csv文件的参数化使用中需要注意的事项。 对于有中文内容&#xff0c;涉及到编码格式&#xff0c;为了让jmeter能正确地读取csv文件中的中文&#xff0c;需要把文件转码为UTF-8BOM…

数据仓库基础概念

数据仓库 概念 数据仓库&#xff08;Data Warehouse, DW&#xff09;是一个面向主题的、集成的、相对稳定的、反映历史变化的数据集合。它是为满足企业决策分析需求而设计的。 面向主题&#xff1a;数据仓库围绕特定的主题组织数据&#xff0c;例如“销售”或“人力资源”&am…

【网络】详解TCP协议的延时应答、捎带应答、异常处理

【网络】详解TCP协议的延时应答和捎带应答 一. 延时应答模型 二. 捎带应答模型再谈四次挥手 三. 异常处理1.一方出现进程崩溃2.一方出现关机&#xff08;正常流程关机&#xff09;3.一方出现断电4.网线断开 一. 延时应答 也是基于滑动窗口&#xff0c;想要尽可能的去提高效率。…

mysql高级sql语句 二

目录 一. 求交集 1.1 内连接 1.2 左连接 1.3 右连接 1.4 子查询 1.5 多表查询 1.6 并集分组 二. 求差集 2.1 求左表差集 2.2 求右表差集 2.3 求两个表的差集 三. 视图表view 3.1 视图表的使用 3.2 视图表里的数据能不能修改&#xff1f; 四. case语句 五. 无值…

豪威集团技术突破

巴塞罗那AutoSens展会上发布的OX12A10采用豪威集团全新的a-CSP™超小尺寸封装技术&#xff0c;是TheiaCel™产品系列中分辨率最高的传感器&#xff0c;成为ADAS和AD的理想之选 巴塞罗纳&#xff0c;西班牙 – 2024年10月3日 – 豪威集团&#xff0c;全球排名前列的先进数字成像…

动力学的开环和闭环控制

工业机器人四大元件&#xff1a;控制器&#xff0c;驱动器&#xff0c;电机&#xff0c;减速器 流程&#xff1a;控制器的作用是规划一个机器人的路径&#xff0c;位置&#xff0c;速度。而驱动器是用来控制电流的&#xff0c;进行控制电机。本质上是驱动器来进行完成电流的调…

ssm基于SSM框架的成绩管理系统的设计与实现+vue

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习&#xff0c;获取源码请私聊我 需要定制请私聊 目 录 1 绪论 1 1.1 选题背景 1 1.2 选题意义 1 1.3 研究内容 2 2 系统开发技术 3 2.1 MySQL数据库 3 2.…

【系统集成中级】OSI 七层模型

【系统集成中级】OSI 七层模型 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; OSI 七层模型&#xff1a; #mermaid-svg-FqFAWaiBSmivKOt2 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mer…