js 社区存在多种模块化规范,其中最常使用到的是 node 本身实现的 commonjs 和 es6 标准的 esm。

commonjs 和 esm 存在多种根本上的区别,详细的比较在阮一峰的《es6标准入门》已经写得很详细了,这里我想用自己的思路重新总结一下。同时分析一下 babel 对于 esm 的编译转换,存在的局限。

commonjs 和 esm 的主要区别可以概括成以下几点:

  1. 输出拷贝 vs 输出引用
  2. esm 的 import read-only 特性
  3. 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 的区别相关推荐

  1. CommonJs 和 ESModule 的 区别整理

    1. exports 和 module.exports 的区别 module.exports 默认值为{} exports 是 module.exports 的引用 exports 默认指向 modu ...

  2. CommonJs和ESModule的区别及优缺点

    说起js,就不得不提及模块化编程,简单来说,模块化就像造汽车一样,有人造轮子,有人早车外壳,最后将各自造好的东西拼到一起组装成汽车,充分体现了分工合作的思想. 模块化的作用 使用模块化是为了将一个很大 ...

  3. commonjs 和esm

    模块化 commonjs是静态的,基础变量的话,不会随着模块中的值改变,那么引用模块到外的值 有所改变. esm则是动态的,如果模块里边的值改变了,那么引用模块到外的值也会改变. cmd amd co ...

  4. JS 模块化: CommonJS 与 ESM(ECMAScript Module) 的引用机制比较 循环依赖解决方式

    JS 模块化: CommonJS 与 ESM(ECMAScript Module) 的引用机制比较 & 循环依赖解决方式 文章目录 JS 模块化: CommonJS 与 ESM(ECMAScr ...

  5. 面试题:Commonjs 和 Es Module区别

    一 前言 今天我们来深度分析一下 Commonjs 和 Es Module,希望通过本文的学习,能够让大家彻底明白 Commonjs 和 Es Module 原理,能够一次性搞定面试中遇到的大部分有关 ...

  6. JS高级笔记:CommonJs与ESModule的区别

    区别: 两者的模块导入导出语法不同,CommonJs是通过module.exports,exports导出,require导入:ESModule则是export导出,import导入. CommonJ ...

  7. 模块化开发:AMD、CMD、CommonJs和Es6的区别

    什么是AMD.CMD.CommonJs? 他们之间有什么区别? 项目当中都是如何运用的? AMD即Asynchronous Module Definition,是RequireJS在推广过程中对模块定 ...

  8. 前端模块化iife、CJS、AMD、UMD、ESM的区别

    前端模块化 注:以下所有解释完全依照本人的主观思想,如果有不对的地方,请见谅 说到模块化,不得不先了解一下模块的起源,时间顺序方面不要太在意 初始,只是创建一个js文件,里面定义一些方法.常量等,提供 ...

  9. JavaScript核心知识总结(下)

    前言 这是这个系列的最后一篇,讲述的是一些 JavaScript 的进阶知识,也算是复习学习重点了. 异常处理 为什么要处理异常 增强用户体验: 远程定位问题: 未雨绸缪,及早发现问题: 无法复现问题 ...

最新文章

  1. 知识产权基础(上、下)
  2. java文件在没有安装jdk的windows下运行。
  3. ln -s 的一个坑
  4. React开发(174):ant design按钮确认删除
  5. 【转】RAX,eax,ax,ah,al 关系
  6. 让OpenShift Serivce Mesh自动对服务注入sidecar
  7. 画PCB开始前的准备工作
  8. 机顶盒ttl无法输入_一个作业,多个TTL——Flink SQL 细粒度TTL配置的实现(二)
  9. 动易 当前服务器不允许上传文件,动易网站详细安说明及常见疑难解答.doc
  10. 数据库系统原理课程设计
  11. Android DeviceOwner 应用的能力
  12. [iOS]Xcode8 搭建 .framework
  13. 各种光纤接口类型介绍
  14. 实现国产化转型,ZStack Cloud 助力中铁财务数字化转型!
  15. 信息年龄、新鲜度、数据寿命、边缘计算等读书报告
  16. 【win10系统重装】
  17. android app 运行时提示 应用专为旧版 Android 打造
  18. 兰德系数(Rand Index)
  19. win10 程序最小化不在任务栏了?在左下角
  20. SAP MM 执行事务代码VL10B 报错-4501378483 000010 Only 0 CS of material ### available-

热门文章

  1. Deepin系统个人评测
  2. 频次直方图、数据区间划分额分布密度——Note_6
  3. 深圳地铁APP伴您行,妥妥的
  4. [Linux] 什么是 段错误(吐核)?
  5. for example: not eligible for auto-proxying问题解决
  6. shell脚本实现分日志级别输出
  7. 色卡司 NAS存储器设置
  8. vue.runtime.esm.js?2b0e:619 [Vue warn]: Duplicate keys detected: ‘tab-Test3‘. This may cause an upda
  9. MATLAB(1)基础知识
  10. 爱立信联合SK电讯和宝马进行首次多车辆5G测试