请问: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 = 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 模块

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

分布式链路追踪原理:

我的后端学习大纲 SpringCloud学习大纲 假定三个微服务调用的链路如下图所示&#xff1a;Service 1 调用 Service 2&#xff0c;Service 2 调用 Service 3 和 Service 4 1、完整的调用链路&#xff1a; 1.1.原理分析&#xff1a; 1.那么一条链路追踪会在每个服务调用的时候加…

在PC端使用微信浏览器的调试功能

首先&#xff0c;此功能只限自己开发网页&#xff0c;其次&#xff0c;这是为了帮助使用了微信的相关JS SDK功能&#xff0c;比如微信登录&#xff0c;在不方便使用电脑上的浏览器时使用的的。 方法&#xff1a; 在网页中插入 <script src"https://unpkg.com/vconso…

Java根据word 模板,生成自定义内容的word 文件

Java根据word 模板&#xff0c;生成自定义内容的word 文件 背景1 使用技术2 实现方法依赖啊 3 问题4 背景 主要是项目中需要定制化一个word&#xff0c;也就是有一部分是固定的&#xff0c;就是有一个底子&#xff0c;框架&#xff0c;里面的内容是需要填充的。然后填充的内容…

WPF常见容器全方位介绍

Windows Presentation Foundation (WPF) 是微软的一种用于构建Windows桌面应用程序的UI框架。WPF的布局系统基于容器&#xff0c;帮助开发者以灵活、响应的方式组织用户界面 (UI) 元素。本篇文章将详细介绍WPF中几种常见的容器&#xff0c;包括Grid、StackPanel、WrapPanel、Do…

基于51单片机的proteus数字时钟仿真设计

注意&#xff1a;本项目是本人大学时期的课设项目&#xff0c;不得在未经本人允许下进行转载或商用 数字钟设计 项目背景与意义 在信息化时代&#xff0c;时间管理成为了我们日常生活中不可或缺的一部分。数字钟作为一种常见的时间显示设备&#xff0c;因其精确、直观、易读等…

如何捕捉行情爆发的前兆

在金融市场的激烈角逐中&#xff0c;每一次行情的爆发都是投资者获取丰厚回报的关键时刻。然而&#xff0c;如何识别并把握这些时刻&#xff0c;却是一门需要深厚金融专业知识和敏锐洞察力的艺术。今天&#xff0c;我们就来深入探讨行情爆发的初期信号&#xff0c;揭示那些能够…

Jlink 直接读取单片机数据

1. 驱动版本 因人而异&#xff0c;这里我使用的是 “J-Flash V6.96” 本人驱动链接&#xff1a;夸克网盘 提取码&#xff1a;rgzk 2. 打开软件 3. 创建jlink工程 4. 选择芯片 此处本人使用芯片 “STM32F103VCT6” 5. 连接单片机 连接成功反馈 6. 读取单片机内部数据 …

【2024|FTransUNet|论文解读1】融合视界:解密FTransUNet在遥感语义分割中的创新突破

【2024|FTransUNet|论文解读1】融合视界&#xff1a;解密FTransUNet在遥感语义分割中的创新突破 【2024|FTransUNet|论文解读1】融合视界&#xff1a;解密FTransUNet在遥感语义分割中的创新突破 文章目录 【2024|FTransUNet|论文解读1】融合视界&#xff1a;解密FTransUNet在遥…

web 0基础第四节 多媒体标签

图片标签 主要是讲解 在html 中 怎么将图片放入其中 <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width, initial-scale1.0"> <…

Django模型优化

1、创建一个Django项目 可参考之前的带你快速体验Django web应用 我使用的是mysql数据库。按照上述教程完成准备工作。 2、创建一个app并完成注册 demo主要来完成创建用户、修改用户、查询用户、删除用户的操作。 python manage.py startapp test0023、app的目录 新建templ…

【Spring AI】Java实现类似langchain的第三方函数调用_原理与详细示例

Spring AI 介绍 &#xff1a;简化Java AI开发的统一接口解决方案 在过去&#xff0c;使用Java开发AI应用时面临的主要困境是没有统一且标准的封装库&#xff0c;导致开发者需要针对不同的AI服务提供商分别学习和对接各自的API&#xff0c;这增加了开发难度与迁移成本。而Sprin…

【文献综述】扩散模型在文本生成中的进展

【文献综述】扩散模型在文本生成中的进展 Diffusion models in text generation: a survey 摘要&#xff1a; 扩散模型是一种基于数学的模型&#xff0c;最初应用于图像生成。最近&#xff0c;他们对自然语言生成&#xff08;NLG&#xff09;产生了广泛的兴趣&#xff0c;这是…

一起搭WPF架构之livechart的MVVM使用介绍

一起搭WPF架构之livechart使用介绍 前言ModelViewModelView界面设计界面后端 效果总结 前言 简单的架构搭建已经快接近尾声了&#xff0c;考虑设计使用图表的形式将SQLite数据库中的数据展示出来。前期已经介绍了livechart的安装&#xff0c;今天就详细介绍一下livechart的使用…

03 设计模式-创造型模式-单例模式

单例模式&#xff08;Singleton Pattern&#xff09;是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式。 这种模式涉及到一个单一的类&#xff0c;该类负责创建自己的对象&#xff0c;同时确保只有单个对象被创建…

HarmonyOS开发(State模型)

一、State模型概述 FA&#xff08;Feature Ability&#xff09;模型&#xff1a;从API 7开始支持的模型&#xff0c;已经不再主推。 Stage模型&#xff1a;从API 9开始新增的模型&#xff0c;是目前主推且会长期演进的模型。在该模型中&#xff0c;由于提供了AbilityStage、Wi…

【MR开发】在Pico设备上接入MRTK3(二)——在Unity中配置Pico SDK

上一篇文档介绍了 【MR开发】在Pico设备上接入MRTK3&#xff08;一&#xff09;在Unity中导入MRTK3依赖 下面将介绍在Unity中导入Pcio SDK的具体步骤 在Unity中导入Pico SDK 当前Pico SDK版本 Unity交互SDK git仓库&#xff1a; https://github.com/Pico-Developer/PICO-Un…

基于SpringBoot+Vue+uniapp微信小程序的垃圾分类系统的详细设计和实现(源码+lw+部署文档+讲解等)

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

面试题:Redis(七)

1. 面试题 2. 缓存预热 当Redis暂时没有数据&#xff0c;但MySQL中有数据时&#xff0c;由程序员、中间件、写段程序提前访问该数据&#xff0c;使得数据进行回写进Redis&#xff0c;从而达到缓存预热的效果&#xff0c;这样可以使得一开始访问页面程序的用户也没有卡顿&#x…

机器学习核心:监督学习与无监督学习

个人主页&#xff1a;chian-ocean 文章专栏 监督学习与无监督学习&#xff1a;深度解析 机器学习是现代人工智能的核心支柱&#xff0c;已广泛应用于从数据挖掘到计算机视觉再到自然语言处理的诸多领域。作为机器学习最主要的两大类型&#xff0c;监督学习&#xff08;Super…

自定义注解和组件扫描在Spring Boot中动态注册Bean(一)

​ 博客主页: 南来_北往 系列专栏&#xff1a;Spring Boot实战 在Spring Boot中&#xff0c;自定义注解和组件扫描是两种强大的机制&#xff0c;它们允许开发者以声明性的方式动态注册Bean。这种方式不仅提高了代码的可读性和可维护性&#xff0c;还使得Spring Boot应用的…