commonjs 与 esm 的区别
js 社区存在多种模块化规范,其中最常使用到的是 node 本身实现的 commonjs 和 es6 标准的 esm。
commonjs 和 esm 存在多种根本上的区别,详细的比较在阮一峰的《es6标准入门》已经写得很详细了,这里我想用自己的思路重新总结一下。同时分析一下 babel 对于 esm 的编译转换,存在的局限。
commonjs 和 esm 的主要区别可以概括成以下几点:
- 输出拷贝 vs 输出引用
- esm 的 import read-only 特性
- esm 存在 export/import 提升
下面对这三点做具体分析。
输出拷贝 vs 输出引用
首先看个 commonjs 输出拷贝的例子:
// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {a = 2;b = { num: 2 };
}, 200);
module.exports = {a,b,
};// main.js
// node main.js
let {a, b} = require('./a');
console.log(a); // 1
console.log(b); // { num: 1 }
setTimeout(() => {console.log(a); // 1console.log(b); // { num: 1 }
}, 500);
复制代码
所谓输出拷贝,如果了解过 node 或者 webpack 对 commonjs 的实现(不了解可以看我之前的文章),就会知道:exports 对象是模块内外的唯一关联, commonjs 输出的内容,就是 exports 对象的属性,模块运行结束,属性就确定了。
再看 esm 输出引用的例子:
// a.mjs
let a = 1;
let b = { num: 1 }
setTimeout(() => {a = 2;b = { num: 2 };
}, 200);
export {a,b,
};// main.mjs
// node --experimental-modules main.mjs
import {a, b} from './a';
console.log(a); // 1
console.log(b); // { num: 1 }
setTimeout(() => {console.log(a); // 2console.log(b); // { num: 2 }
}, 500);
复制代码
这就是 esm 输出引用跟 commonjs 输出值的区别,模块内部引用的变化,会反应在外部,这是 esm 的规范。
esm 的 import read-only 特性
read-only 的特性很好理解,import 的属性是只读的,不能赋值,类似于 const 的特性,这里就不举例解释了。
esm 存在 export/import 提升
esm 对于 import/export 存在提升的特性,具体表现是规范规定 import/export 必须位于模块顶级,不能位于作用域内;其次对于模块内的 import/export 会提升到模块顶部,这是在编译阶段完成的。
esm 的 import/export 提升在正常情况下,使用起来跟 commonjs 没有区别,因为一般情况下,我们在引入模块的时候,都会在模块的同步代码执行完才获取到输出值。所以即使存在提升,也无法感知。
所以要想验证这个事实,需要考虑到循环依赖的情况。循环依赖指的是模块A依赖模块B,模块B又依赖模块A,互相依赖产生了死循环。所以各个模块方案本身设计了一套规则来解决这个问题。在循环依赖的情况下,模块会出现执行中断,然后我们可以看到 import/export 提升和 commonjs 的区别。
这里用2个循环依赖的例子来解释,首先看 commonjs 的表现:
// a.js
exports.done = false;
let b = require('./b');
console.log('a.js: b.done = %j', b.done); // true
exports.done = true;
console.log('a.js执行完毕');// b.js
exports.done = false;
let a = require('./a');
console.log('b.js: a.done = %j', a.done); // false
exports.done = true;
console.log('b.js执行完毕');// main.js
let a = require('./a');
let b = require('./b');
console.log('main.js: a.done = %j, b.done = %j', a.done, b.done); // true true// 输出结果
// node main.js
b.js: a.done = false
b.js执行完毕
a.js: b.done = true
a.js执行完毕
main.js: a.done = true, b.done = true
复制代码
这是《es6入门》里的循环依赖的例子,这个例子能提现 commonjs 运行时加载的情况。因为 a.js 依赖 b.js,b.js 又依赖 a.js,所以当 b.js 执行到require('./a')
的时候,a.js 会暂停执行,所以此时require('./a')
返回的是false,但是在main.js中,a.js的返回值又是true
,所以这说明了 commonjs 模块的 exports 是动态执行的,具体 require 能获取到的值,取决于模块的运行情况。
下面是 esm 的循环依赖的例子:
// a.mjs
export let a_done = false;
import { b_done } from './b';
console.log('a.js: b.done = %j', b_done);
console.log('a.js执行完毕');// b.mjs
import { a_done } from './a';
console.log('b.js: a.done = %j', a_done);
export let b_done = true;
console.log('b.js执行完毕');// main.mjs
import { a_done } from './a';
import { b_done } from './b';
console.log('main.js: a.done = %j, b.done = %j', a_done, b_done);// 输出结果
// node --experimental-modules main.mjs
ReferenceError: a_done is not defined
复制代码
这里解释一下,为什么a_done is not defined
。a.mjs 加载 b.mjs,而 b.mjs 又加载 a.mjs,这就形成了循环依赖。循环依赖产生时,a.mjs 中断执行,这时在 b.mjs 中a_done
的值是什么呢?这就要考虑到 a.mjs 的 import/export 提升的问题,a.mjs 中的export a_done
被提升到顶部,执行到import './b'
时,执行权限移交到 b.mjs,此时a_done
只是一个指定导出的接口,但是未定义,所以出现引用报错。
这里先提一下,如果用 babel 来编译执行,是不会报错的,执行结果如下:
// npx babel-node src/main.mjs
b.js: a.done = undefined
b.js执行完毕
a.js: b.done = true
a.js执行完毕
main.js: a.done = false, b.done = true
复制代码
为什么呢?后面会来分析。
bebel 模拟 esm
这一节来看看,babel 是怎么实现 esm 这几个特性的:输出引用、read-only。
还是上面的例子,稍微改一下:
// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {a = 2;b = { num: 2 };
}, 200);
export {a,b,
};// main.js
import {a, b} from './a';
console.log(a);
console.log(b);
setTimeout(() => {console.log(a);console.log(b);
}, 500);a = 3;
复制代码
用babel编译一下,生成了如下的内容:
// a.js
"use strict";
Object.defineProperty(exports, "__esModule", {value: true
});
exports.b = exports.a = void 0;
var a = 1;
exports.a = a;
var b = {num: 1
};
exports.b = b;
setTimeout(function () {exports.a = a = 2;exports.b = b = {num: 2};
}, 200);// main.js
"use strict";
var _a = require("./a");
console.log(_a.a);
console.log(_a.b);
setTimeout(function () {console.log(_a.a);console.log(_a.b);
}, 500);
_a.a = (3, function () {throw new Error('"' + "a" + '" is read-only.');
}());
复制代码
简单分析一下,对于输出引用,babel 是通过在输出属性变化时,同步修改 exports 对象对应的属性来实现的,比如像这样的代码:
exports.a = a = 2;
另外一个特性 read-only,babel 通过抛异常的方式来实现,比如这样的代码:
_a.a = (3, function () {throw new Error('"' + "a" + '" is read-only.');
}());
复制代码
bebel 模拟 esm 的局限
前面关于 esm 的 import/export 提升的例子,在 node 原生 esm 环境下和babel 编译环境下的执行结果不一致,这是什么原因呢?我们把前面的例子用 babel 编译一下,看看转换成什么形式的代码。
首先还是贴一下 esm 代码:
// a.mjs
export let a_done = false;
import { b_done } from './b';
console.log('a.js: b.done = %j', b_done);
console.log('a.js执行完毕');// b.mjs
import { a_done } from './a';
console.log('b.js: a.done = %j', a_done);
export let b_done = true;
console.log('b.js执行完毕');// main.mjs
import { a_done } from './a';
import { b_done } from './b';
console.log('main.js: a.done = %j, b.done = %j', a_done, b_done);
复制代码
用 babel 编译一下,生成了如下的内容:
// a.js
"use strict";
Object.defineProperty(exports, "__esModule", {value: true
});
exports.a_done = void 0;
var _b = require("./b");
var a_done = false;
exports.a_done = a_done;
console.log('a.js: b.done=%j', _b.b_done);
console.log('a.js执行完毕');// b.js
"use strict";
Object.defineProperty(exports, "__esModule", {value: true
});
exports.b_done = void 0;
var _a = require("./a");
console.log('b.js: a.done=%j', _a.a_done);
var b_done = true;
exports.b_done = b_done;
console.log('b.js执行完毕');// main.js
"use strict";
var _a = require("./a");
var _b = require("./b");
console.log('main.js: a.done=%j, b.done=%j', _a.a_done, _b.b_done);
复制代码
可以看到,babel 也实现了 export 的提升,输出值统一设置为void 0
,但是想象一下,a_done
其实是 export 对象的属相,那么在 commonjs 的环境下,从对象取值,只可能会出现undefined
,而不可能出现is not defined
。
其实根本原因也是源于 commonjs 输出的是对象,而 esm 输出的是引用,babel 本质是利用 commonjs 来模拟 esm,所以这个特性也是 babel 无法模拟实现的。
结论
本文主要总结了 commonjs 跟 esm 的主要对比,并且分析了 babel 模拟 esm 的方式和局限。
文章主要是个人的理解和总结,如有错误欢迎指正。
转载于:https://juejin.im/post/5cf8e159f265da1b94213a63
commonjs 与 esm 的区别相关推荐
- CommonJs 和 ESModule 的 区别整理
1. exports 和 module.exports 的区别 module.exports 默认值为{} exports 是 module.exports 的引用 exports 默认指向 modu ...
- CommonJs和ESModule的区别及优缺点
说起js,就不得不提及模块化编程,简单来说,模块化就像造汽车一样,有人造轮子,有人早车外壳,最后将各自造好的东西拼到一起组装成汽车,充分体现了分工合作的思想. 模块化的作用 使用模块化是为了将一个很大 ...
- commonjs 和esm
模块化 commonjs是静态的,基础变量的话,不会随着模块中的值改变,那么引用模块到外的值 有所改变. esm则是动态的,如果模块里边的值改变了,那么引用模块到外的值也会改变. cmd amd co ...
- JS 模块化: CommonJS 与 ESM(ECMAScript Module) 的引用机制比较 循环依赖解决方式
JS 模块化: CommonJS 与 ESM(ECMAScript Module) 的引用机制比较 & 循环依赖解决方式 文章目录 JS 模块化: CommonJS 与 ESM(ECMAScr ...
- 面试题:Commonjs 和 Es Module区别
一 前言 今天我们来深度分析一下 Commonjs 和 Es Module,希望通过本文的学习,能够让大家彻底明白 Commonjs 和 Es Module 原理,能够一次性搞定面试中遇到的大部分有关 ...
- JS高级笔记:CommonJs与ESModule的区别
区别: 两者的模块导入导出语法不同,CommonJs是通过module.exports,exports导出,require导入:ESModule则是export导出,import导入. CommonJ ...
- 模块化开发:AMD、CMD、CommonJs和Es6的区别
什么是AMD.CMD.CommonJs? 他们之间有什么区别? 项目当中都是如何运用的? AMD即Asynchronous Module Definition,是RequireJS在推广过程中对模块定 ...
- 前端模块化iife、CJS、AMD、UMD、ESM的区别
前端模块化 注:以下所有解释完全依照本人的主观思想,如果有不对的地方,请见谅 说到模块化,不得不先了解一下模块的起源,时间顺序方面不要太在意 初始,只是创建一个js文件,里面定义一些方法.常量等,提供 ...
- JavaScript核心知识总结(下)
前言 这是这个系列的最后一篇,讲述的是一些 JavaScript 的进阶知识,也算是复习学习重点了. 异常处理 为什么要处理异常 增强用户体验: 远程定位问题: 未雨绸缪,及早发现问题: 无法复现问题 ...
最新文章
- 知识产权基础(上、下)
- java文件在没有安装jdk的windows下运行。
- ln -s 的一个坑
- React开发(174):ant design按钮确认删除
- 【转】RAX,eax,ax,ah,al 关系
- 让OpenShift Serivce Mesh自动对服务注入sidecar
- 画PCB开始前的准备工作
- 机顶盒ttl无法输入_一个作业,多个TTL——Flink SQL 细粒度TTL配置的实现(二)
- 动易 当前服务器不允许上传文件,动易网站详细安说明及常见疑难解答.doc
- 数据库系统原理课程设计
- Android DeviceOwner 应用的能力
- [iOS]Xcode8 搭建 .framework
- 各种光纤接口类型介绍
- 实现国产化转型,ZStack Cloud 助力中铁财务数字化转型!
- 信息年龄、新鲜度、数据寿命、边缘计算等读书报告
- 【win10系统重装】
- android app 运行时提示 应用专为旧版 Android 打造
- 兰德系数(Rand Index)
- win10 程序最小化不在任务栏了?在左下角
- SAP MM 执行事务代码VL10B 报错-4501378483 000010 Only 0 CS of material ### available-
热门文章
- Deepin系统个人评测
- 频次直方图、数据区间划分额分布密度——Note_6
- 深圳地铁APP伴您行,妥妥的
- [Linux] 什么是 段错误(吐核)?
- for example: not eligible for auto-proxying问题解决
- shell脚本实现分日志级别输出
- 色卡司 NAS存储器设置
- vue.runtime.esm.js?2b0e:619 [Vue warn]: Duplicate keys detected: ‘tab-Test3‘. This may cause an upda
- MATLAB(1)基础知识
- 爱立信联合SK电讯和宝马进行首次多车辆5G测试