ESM对比CommonJS

本文对比了ESM(异步模块)与CommonJS在模块加载方式、依赖解析时机、导出值类型以及执行环境中的差异,强调了ESM的异步特性如何解决模块加载阻塞问题,以及它们在严格模式下的不同行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

以下内容均总结自es-modules-a-cartoon-deep-dive

1. ESM是异步的而CommonJS是同步的

异步是ESM面临的最大挑战。和CommonJS的执行环境Node获取模块文件的方式不同,ESM获取模块文件是通过网络。如果ESM从获取到执行所有模块都是顺序进行会导致主线程长期处于pending状态,无法响应用户事件。

import "https://example.com/main.js"
console.log("我在等上面导入的模块执行完成")

为了解决这个问题ESM将模块的解析和执行分成了三部分,分别是构建(Construction),实例化(Instantiation)和执行(Evaluation)。
浏览器在构建阶段会通过入口模块(<script type="module" src="https://example.com/main.js" />)下载所有依赖的模块生成模块的依赖图。这部分工作不会阻塞主线程,此时主线程可以响应用户的点击滚动等事件。
而CommonJS则无此顾虑。Node脚本运行在服务器中,而对应的模块文件也可以在服务器的磁盘中获取而无需通过网络,所以模块加载和执行可以同步进行,没有加载时间过长阻塞主线程的顾虑。

2. ESM是编译时解析依赖而CommonJS是运行时解析依赖

一个字符串"https://example.com/main.js"在编译时可以被理解为字符串,在运行时也是一个字符串,但是如果是一个变量名pathname在编译时和运行时含义则大不相同。该变量在代码没有执行到这块时无法确定其具体值。这就导致ESM的module specifier(下图部分,图片来自es-modules-a-cartoon-deep-dive),无法使用变量。
在这里插入图片描述
例:

import {count} from "./counter.js" // ok
import {count} from `${pathname}.js` // error

在CommonJS中module specifier中存在变量是合法的,因为CommonJS中模块的解析是在运行时,在运行时标识符pathname可以计算出具体值。

const pathname = './counter.js'
require(`${pathname}.js`) // ok

3. ESM导出的是引用而CommonJS导出的是值拷贝

在ESM中导入的变量会跟着导出的变动而变动。

ESM

// module.js
export let a = 1;
export function changeA() {a = 10;}

// entry.js
import {a, changeA} from './module.js';
changeA();
console.log(a); // 10

变量a在原模块中变成10后,在导入模块entry.js中也会跟着变化。所以在ESM中模块导出的是引用,具体实现可参考es-modules-a-cartoon-deep-dive

CommonJS

// module.js
let a = 1;
const changeA = function() {a = 10;}
exports.a = a;
exports.changeA = changeA;

// entry.js
const {a} = require('./module.js');
module.changeA()
console.log(a) // 1

变量在原模块中变成10之后导入模块entry.js中变量a并没有跟着变化,所以在ESM中模块导出的是值。

补充

可以将JS中的变量简单理解为一个盒子,这个盒子中可以存放数字,字符串等。

const a = 1;
const b = a;

上例中声明了一个变量a对应一个盒子,这个盒子里面放的是数字1,然后又声明了一个盒子b,这个b中放的是从盒子a中拿出来的值1。所以当盒子a中的值从1变成2的时候b盒子中的1并不会跟着变化。
JS的变量对应的盒子中不止能放值,还能放一个盒子。

const a = {value: 1};
const b = a;

上例中声明了一个a盒子,里面放了一个盒子{value: 1},然后又声明了一个盒子b,里面放的也是盒子{value: 1}。这时当盒子{value: 1}value值从1变成2ab盒子都能取到最新值。
所以ESM导出的是这个盒子,而CommonJS导出的是值。具体实现可参考es-modules-a-cartoon-deep-dive

4. ESM执行在严格模式下而CommonJS未自动开启严格模式

在ESM下从入口开始解析的所有脚本都默认按照严格模式解析,其影响包括但不限于:

  1. 变量必须声明后再使用
  2. 函数的参数不能有同名属性,否则报错
  3. 不能使用with语句
  4. 不能对只读属性赋值,否则报错
  5. 不能使用前缀 0 表示八进制数,否则报错
  6. 不能删除不可删除的属性,否则报错
  7. 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  8. eval不会在它的外层作用域引入变量
  9. eval和arguments不能被重新赋值
  10. arguments不会自动反映函数参数的变化
  11. 不能使用arguments.callee
  12. 不能使用arguments.caller
  13. 禁止this指向全局对象
  14. 不能使用fn.caller和fn.arguments获取函数调用的堆栈
  15. 增加了保留字(比如protected、static和interface)

以上限制信息来自于Module 的语法
对于JS模块文件的识别是通过<script type="module" src="https://example.com/main.js" />入口文件对应标签上的属性type="module"识别的,被该入口模块引用的所有模块依赖都会被当做模块解析。

而在CommonJS中因为没有script标签作为入口明确识别模块文件,所以事情变的些许复杂,查阅相关资料表明可以通过.mjs后缀名来让Node明白这是一个ESM文件,可以启用严格模式。

参考

  1. es-modules-a-cartoon-deep-dive
  2. Module 的语法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值