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

前言

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

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

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.rhkb.cn/news/451986.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AnaTraf | TCP重传的工作原理与优化方法

目录 什么是TCP重传&#xff1f; TCP重传的常见触发原因 TCP重传对网络性能的影响 1. 高延迟与重传 2. 吞吐量的下降 如何优化和减少TCP重传 1. 优化网络设备配置 2. 优化网络链路 3. 网络带宽的合理规划 4. 部署CDN和缓存策略 结语 AnaTraf 网络性能监控系统NPM | …

不收费的数据恢复工具有哪些好用?快来看这五款:

大家好&#xff0c;今天我来跟大家分享一下使用不收费数据恢复软件的一些心得和体验&#xff1b;数据丢失是一件非常让人头疼的事情&#xff0c;尤其是对于那些重要文件来说&#xff1b;幸好&#xff0c;现在市面上有不少不收费的数据恢复软件可以帮助我们找回丢失的数据。接下…

Thread的基本用法

创建线程 方法一 继承Thread类 继承 Thread 来创建一个线程类. class MyThread extends Thread {Overridepublic void run() {System.out.println("这里是线程运行的代码");} } 创建 MyThread 类的实例 MyThread t new MyThread(); 调用 start 方法启动线程 t…

Java项目-基于Springboot的招生管理系统项目(源码+说明).zip

作者&#xff1a;计算机学长阿伟 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、ElementUI等&#xff0c;“文末源码”。 开发运行环境 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/…

【C语言刷力扣】2006.差的绝对值为K的数对数目

题目; 法一 解题思路&#xff1a; |num[i] - num[j]| k 可以理解为 num[j] num[i] k 和 num[j] num[i] - k 两种情况。 int countKDifference(int* nums, int numsSize, int k) {int ans 0;int hash[101];memset(hash, 0, sizeof(hash));for (int i 0; i < numsSize;…

【GAMES101笔记速查——Lecture 16 Ray Tracing4】

上节课的内容&#xff1a;辐射度量学、光线传播、反射方程、渲染方程、全局光照、概率论复习 这节课要介绍一种真实的渲染方法-蒙特卡洛路径追踪 目录 1 简单回顾 1.1 渲染方程&#xff08;The Rendering Equation&#xff09; 1.2 概率 2 蒙特卡洛积分&#xff08;Monte…

无人机初识及应用概览

随着科技的飞速发展&#xff0c;无人机作为一种新兴技术产品&#xff0c;逐渐走进了我们的日常生活和学习中。以下是对该文内容的全面总结&#xff0c;旨在帮助读者更好地理解和认识无人机的基本概念、分类、应用领域、国产标杆品牌以及四旋翼无人机的具体组成。 一、无人机的概…

第23章 - Elasticsearch 洞悉你的查询:如何在上线前发现潜在问题!

文章目录 1. 前言2. Profile API - 查询优化2.1 Profile API 简单介绍2.2 查询结果图形化2.3 Profile 注意事项 3. Explain API - 解释查询 1. 前言 在第 21 章中&#xff0c;我介绍了 Elasticsearch 的读优化&#xff0c;但你是否曾疑惑&#xff1a;如何在上线前判断查询的耗…

FFmpeg 4.3 音视频-多路H265监控录放C++开发二 : 18.04ubuntu安装,linux 下build ffmpeg 4.3 源码 并测试

测试环境 ubuntu 18.04 64 位&#xff0c;安装vmware and ubuntu 安装后调整 分辨率&#xff1a; 让windows 可以和 linux 互相复制黏贴 sudo apt-get autoremove open-vm-tools sudo apt-get update sudo apt-get install open-vm-tools-desktop 一直Y reboot 依赖安装 sud…

【数字图像处理】第5章 图像空域增强方法

上理考研周导师的哔哩哔哩频道 我在频道里讲课哦 目录 5.1 图像噪声 相关概念 ①图像噪声的产生 ② 图像噪声分类 ③ 图像噪声特点 5.2 图像增强方法分类 ①图像增强概念 ②图像增强目的 ③图像增强技术方法: 5.3 基于灰度变换的图像增强 1. 概述: 2. 灰度变换…

HCIP open-Euler学习文档

第一期 操作系统基础&#xff0c;web基础 OpenEuler 目录 学习系统常用应用(Apache Nginx DNS MySQL)服务器集群架构(HAProxy, Nginx, LVS,keepalived)存储管理(GlusterFS,NAS,SAN)自动化基础(Ansible,SaltStack)Shell脚本基础(变量&#xff0c;语法&#xff0c;函数&…

推荐一个处理数据非常好用的在线工具

推荐一个处理数据非常好用的在线工具 只要你的工作用到了电脑&#xff0c;经常需要处理数据的话&#xff0c;那么你肯定会遇到数据各种各样的问题&#xff0c;比如去重&#xff0c;对比&#xff0c;统计&#xff0c;排序等等 身为一名后端程序员&#xff0c;每天就是和数据打…

基于SpringBoot+Vue+uniapp的诗词学习系统的详细设计和实现

详细视频演示 请联系我获取更详细的演示视频 项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不…

大数据-171 Elasticsearch ES-Head 与 Kibana 配置 使用 测试

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

Python案例小练习——小计算器

文章目录 前言一、代码展示二、运行展示 前言 这是用python实现一个简单的计器。 一、代码展示 def calculate(num1, op, num2):if op "":return float(num1) float(num2)elif op "-":return float(num1) - float(num2)elif op "*":return…

vue3--通用 popover 气泡卡片组件实现

背景 在日常开发中,我们一般都是利用一些诸如:element-ui、element-plus、ant-design等组件库去做我们的页面或者系统 这些对于一些后台管理系统来说是最好的选择,因为后台管理系统其实都是大同小异的,包括功能、布局结构等 但是对于前台项目,比如官网、门户网站这些 …

Ubuntu如何显示pcl版本

终端输入&#xff1a; apt-cache show libpcl-dev可以看到&#xff0c;Ubuntu20.04&#xff0c;下载的pcl&#xff0c;应该都是1.10版本的

dayjs日期格式化,开发uniapp或unicloud前后端进行时间格式转换

一、 为什么要用日期格式化 因为在开发项目过程中&#xff0c;会遇到各种各样的日期格式&#xff0c;有的显示完整的年-月-日 时:分:秒&#xff0c;而有的场景就只显示月-日等格式&#xff0c;还有就是显示当前时间和注册时间的间隔时长等&#xff0c;场景非常多&#xff0c;如…

Docker部署Kamailio,并使用LinPhone实现网络通话

前提条件 准备一个路由器&#xff0c;一个服务器&#xff0c;两个终端设备&#xff08;手机或电脑&#xff09; docker部署安装 我使用的是windows系统&#xff0c;docker desktop 先启动Docker desktop打开cmd&#xff0c;输入docker命令docker run --name kamailio --rm…

【MySQL】多表查询——内连接,左/右连接

目录 准备工作 1.多表查询 2.INNER JOIN&#xff08;内连接&#xff09; 2.1.笛卡尔积 1.2.笛卡尔积的过滤 1.3.INNER JOIN&#xff08;显式内连接&#xff09; 1.4.SELF JOIN&#xff08;自连接&#xff09; 3. LEFT JOIN&#xff08;左连接&#xff09; 3.1.一个例子…