【搬运】ES6 module和CommonJS的5点区别

原文:https:※//zhuanlan.※zhihu.com/p/512292465

ES6 module和CommonJS到底有什么区别?

“ES6 module是编译时加载,输出的是接口,CommonJS运行时加载,加载的是一个对象”

这里的“编译时”是什么意思?和运行时有什么区别?“接口”又是什么意思?

“ES6 模块输出的是值的引用,CommonJS 模块输出的是一个值的拷贝”

那么“值的引用”和“值的拷贝”对于开发者又有什么区别呢?

下面通过一些示例详细说明ES6 module和CommonJS的5点区别

1. 编译时导出接口 VS 运行时导出对象

CommonJS 模块是运行时加载,因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。

ES6 模块是它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

这里的“编译时”,指的是js代码在运行之前的编译过程,我们熟悉的变量提升就发生在编译阶段,但是由于编译过程是引擎的行为,开发者没法在编译阶段做任何操作,所以不容易直观地理解“编译时导出接口和运行时导出对象”这个区别。

不过我们在循环引用这个场景就可以轻松地理解两者的差异。

来看下面CommonJS的代码

// index.js
const {log} = require('./lib.js');
module.exports = {
    name: 'index'
};
log();

// lib.js
const {name} = require('./index.js');
module.exports = {
    log: function () {
        console.log(name);
    }
};

执行index.js:node index.js结果会打印"undefined"。

这里index模块和lib相互依赖。

我们分析一下代码执行过程,首先const {log} = require('./lib.js');导入lib.js模块,这时候开始加载lib模块,lib会先导入index,const {name} = require('./index.js');但这个时候index还没有定义name,所以lib中这里的name是undefined,然后lib导出log方法。

接下来index导出name,然后执行log,由于lib中的name是undefined,因此最终结果是打印undefined。

整个过程模块都是运行时加载,代码依次执行,所以很容易分析出执行结果。

而ES6 module有所不同,接下来看一个es6 module的例子。代码内容和上面一样,只是把模块规范从CommonJS换成es6 module。

// index.mjs
import {log} from './lib.mjs';
export const name = 'index';
log();

// lib.mjs
import {name} from './index.mjs';
export const log = () => {
    console.log(name);
};

首先import {log} from './lib.mjs';导入lib模块,注意这个时候虽然没有执行export const name = 'index';,index模块还没有导出name的值,但是index模块已经编译完成,lib已经可以获取到name的引用,只是还没有值。这非常像代码编译阶段的变量提升。

然后加载lib模块,import {name} from './index.mjs';这句导入了index模块的name,(这时候获取到的是name这个引用,但因为还没有值,因此如果马上打印name console.log(name)会报错。)接下来lib导出log方法。

然后index模块执行export const name = 'index';导出name,其值为"index"。

最后执行log方法log();因为name已经赋值,所以lib中name的引用可以正常访问到值"index",所以最终结果是打印"index"。

综上所述,es6 module虽然模块未初始化好时候就被lib导入,但因为获取的是导出的接口(接口编译阶段就已经输出了),等初始化好之后就能使用了。

2. 引用 VS 值拷贝

ES6 module导入的模块不会缓存值,它是一个引用,这个在上面的例子中已经讨论过。 CommonJS会缓存值,这个很好理解,因为js中普通变量是值的拷贝,其实就是把模块中的值赋给一个新的变量。

看下CommonJS的一个例子

// index.js
const {name} = require('./lib.js'); // 等价于const lib = require('./lib'); const {name} = lib;
setTimeout(() => {
    console.log(name); // 'Sam'
}, 1000);

// lib.js
const lib = {
    name: 'Sam'
};
module.exports = lib;
setTimeout(() => {
    lib.name = 'Bob';
}, 500);

index模块中导入lib的nameconst {name} = require('./lib.js');其实就是把lib中的name赋给index里面一个name变量。后面lib中name的变化不会影响到index中的name变量。

而ES6中类似的引用语法,导入的则是引用

// index.mjs
import {name} from './lib.mjs';
setTimeout(() => {
    console.log(name); // 'Bob'
}, 1000);

// lib.mjs
export let name = 'Sam';
setTimeout(() => {
    name = 'Bob';
}, 500);

这里index模块中的name是lib导出的name的引用,因此lib中name变化会同步到index中。

当然这并不意味着ES6 module可以做到比CommonJS更多的事情,因为如果希望在CommonJS中获取到变化,也可以直接访问lib.name。

// index.js
const lib = require('./lib.js');
setTimeout(() => {
    console.log(lib.name); // 'Bob'
}, 1000);

所以这个特性的区别只是需要我们在实现模块时候注意一下,避免预期之外的情况。

其实在上面循环引用的例子中,也能看到CommonJS拷贝值和ES6 module引用的区别,CommonJS因为是拷贝值,所以导入模块时候如果还没初始化好,就是undefined,而ES6 module是引用,所以初始化好之后就可以用了。

3. 静态 VS 动态

ES6 module静态语法和CommonJS的动态语法是很重要的区别,

CommonJS的动态性体现在两个方面

  1. 可以根据条件判断是否加载模块
if (condition) {
    require('./lib');
}
  1. require的模块参数可以是一个变量
require(`./${template}/index.js`);

这种动态性导致依赖关系不好分析,打包工具在静态代码分析阶段不容易知道模块是否需要被加载、模块的哪些部分需要被加载,哪些不会被用到。

相应地,ES6 module的静态性体现在

  1. import必须在顶层
  2. import的模块参数只能是字符串,不能是变量 所以打包工具能够静态分析出依赖关系,并确定知道哪些模块需要被加载、模块的哪些部分被用到。

所以ES6 module静态语法支持打包tree-shaking,而CommonJS不行。

4. 只读 VS 可变

CommonJS导入的模块和普通变量没有区别,ES6 module导入的模块则不同,import导入的模块是只读的。

// demo-commonjs.js
let path = require('path');
path = 1;

// demo-esm.js
import path from 'path';
path = 1; // Error: Cannot assign to 'path' because it is an import

5. 异步 VS 同步

ES6 module支持异步加载,浏览器中会用到该特性,而Commonjs是不支持异步的,因为服务器端不需要异步加载。所以CommonJS不可替代ES6 module,ES6 module可以替代CommonJS。

总结

ES6 module和CommonJS的区别主要有5点

  1. ES6 module是编译时导出接口,CommonJS是运行时导出对象。
  2. ES6 module输出的值的引用,CommonJS输出的是一个值的拷贝。
  3. ES6 module语法是静态的,CommonJS语法是动态的。
  4. ES6 module导入模块的是只读的引用,CommonJS导入的是可变的,是一个普通的变量。
  5. ES6 module支持异步,CommonJS不支持异步。

ES6 module作为新的规范,可以替代之前的AMD、CMD、CommonJS作为浏览器和服务端的通用模块方案。NodeJS在13.2.0版本后已经开始完全支持ES6 module,ES6 module在未来也会实际的模块化标准。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容