請(qǐng)說(shuō)說(shuō)ES6 module和CommonJS的區(qū)別
面試常問(wèn)系列。
”
老規(guī)矩先看目錄:

CommonJS:
1.module代表當(dāng)前模塊:
在CommonJS中,一個(gè)文件就是一個(gè)模塊,模塊中的變量,函數(shù),類都是私有的外部不可以訪問(wèn),并規(guī)定module代表當(dāng)前模塊,exports是對(duì)外的接口。CommonJS主要依賴于module這個(gè)類,我們可以看一下module上面的相關(guān)屬性:
Module {
id: '.', // 如果是 mainModule id 固定為 '.',如果不是則為模塊絕對(duì)路徑
exports: {}, // 模塊最終 exports
filename: '/absolute/path/to/entry.js', // 當(dāng)前模塊的絕對(duì)路徑
loaded: false, // 模塊是否已加載完畢
children: [], // 被該模塊引用的模塊
parent: '', // 第一個(gè)引用該模塊的模塊
paths: [ // 模塊的搜索路徑
'/absolute/path/to/node_modules',
'/absolute/path/node_modules',
'/absolute/node_modules',
'/node_modules'
]
}
2.為什么可以直接使用exports,module,__dirname這些方法屬性?
要回答這個(gè)問(wèn)題我們要從CommonJS內(nèi)部執(zhí)行代碼的原理說(shuō)起。
在CommonJS規(guī)范中代碼在運(yùn)行時(shí)會(huì)被包裹在一個(gè)立即執(zhí)行函數(shù)中,之后我們會(huì)改變這個(gè)立即執(zhí)行函數(shù)中內(nèi)部this的指向,指向的便是module.exports這個(gè)空對(duì)象。這便可以很好的解釋我們node.js中內(nèi)部this指向的是一個(gè)空對(duì)象的問(wèn)題。
邏輯代碼:
(function (exports, require, module, __filename, __dirname) {
let name = "lm";
exports.name = name;
});
jsScript.call(module.exports, args);
之后我們會(huì)給其傳遞exports, require, module,,__filename等參數(shù),所以我們可以在直接在編寫node.js代碼中使用這些變量。
3.exports與module.exports有什么區(qū)別?
在node.js中我們導(dǎo)出一個(gè)變量,函數(shù),或者類一般有兩種導(dǎo)出方法:
function A() {
console.log('過(guò)年好!');
}
// 法一:module.exports.A = A;
// 法二:exports.A = A;
這兩種方法有什么區(qū)別嗎?其實(shí)exports只是module.exports的引用罷了,所以實(shí)際上這兩種方法在使用上的效果是一樣的。
const module = {
'exports': {
}
}
const exports = module.exports;
exports.name = 'Andy'; //完全等價(jià)于 module.exports.name = 'Andy';
所以當(dāng)我們使用exports或者module.exports導(dǎo)出模塊時(shí),其實(shí)也就是給module.exports這個(gè)對(duì)象添加屬性,之后我們使用require引入模塊時(shí)得到的便是module.exports這個(gè)對(duì)象。
注意:既然是對(duì)象屬性的引用,所以當(dāng)我們使用一個(gè)模塊中的方法修改該模塊中的變量,之后導(dǎo)出的變量的結(jié)果是不變的,也就是說(shuō)只要一個(gè)變量已經(jīng)被導(dǎo)出了之后在模塊內(nèi)部對(duì)變量的修改都將無(wú)意義,這個(gè)情況要格外注意。(這點(diǎn)與ES6 module有很大的不同)
a.js:
let count = 1;
function add() {
count += 1;
}
exports.count = count;
exports.add = add;
b.js:
let Module = require('./a');
console.log(Module.count); // 1
Module.add();
console.log(Module.count); // 1
4.模塊引入后自動(dòng)緩存
我們?cè)谑褂胷equire時(shí)可能是這樣的:
let Module = require('./a');
如果是系統(tǒng)模塊,或者第三方模塊我們可以直接寫模塊名:
let fs = require('fs');
但實(shí)際上在require模塊時(shí)我們都是要根據(jù)計(jì)算機(jī)中的絕對(duì)地址來(lái)引入,這個(gè)根據(jù)相對(duì)地址或者包名來(lái)查找文件的過(guò)程是比較消耗時(shí)間的,我們可以通過(guò)module.paths來(lái)打印一下查找的過(guò)程:
[
'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\node_modules',
'c:\\Users\\dell\\Desktop\\node_modules',
'c:\\Users\\dell\\node_modules',
'c:\\Users\\node_modules',
'c:\\node_modules'
]
所以為了提高性能,我們每次在文件中引入一個(gè)模塊時(shí),我們都會(huì)將引入的這個(gè)模塊與其相應(yīng)的絕對(duì)地址進(jìn)行緩存,如果在一個(gè)文件中多次引入相同的模塊這個(gè)模塊只會(huì)被加載一次。
我們可以使用require.cache打印出當(dāng)前模塊的依賴模塊看看,我們可以發(fā)現(xiàn)其是以絕對(duì)地址為key,模塊為value的對(duì)象:
[Object: null prototype] {
'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)\\b.js': Module {
id: '.',
path: 'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)',
exports: {},
parent: null,
filename: 'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)\\b.js',
loaded: false,
children: [ [Module] ],
paths: [
'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\node_modules',
'c:\\Users\\dell\\Desktop\\node_modules',
'c:\\Users\\dell\\node_modules',
'c:\\Users\\node_modules',
'c:\\node_modules'
]
},
'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)\\a.js': Module {
id: 'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)\\a.js',
path: 'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)',
exports: { count: 1, add: [Function: add] },
parent: Module {
id: '.',
path: 'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)',
exports: {},
parent: null,
filename: 'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)\\b.js',
loaded: false,
children: [Array],
paths: [Array]
},
filename: 'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)\\a.js',
loaded: true,
children: [],
paths: [
'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\字節(jié)跳動(dòng)\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\前端相關(guān)題目\\node_modules',
'c:\\Users\\dell\\Desktop\\web-design\\node_modules',
'c:\\Users\\dell\\Desktop\\node_modules',
'c:\\Users\\dell\\node_modules',
'c:\\Users\\node_modules',
'c:\\node_modules'
]
}
}
從而可以很好的解釋這個(gè)例子:
// a.js
module.exports = {
foo: 1,
};
// main.js
const a1 = require('./a.js');
a1.foo = 2;
const a2 = require('./a.js');
console.log(a2.foo); // 2
console.log(a1 === a2); // true
我們可以理解為只要模塊一引入加載完,即使再次引入也還是之前的模塊。
同時(shí)緩存還很好的解決了循環(huán)引用的問(wèn)題:舉個(gè)例子,現(xiàn)在有模塊 a require 模塊 b;而模塊 b 又 require 了模塊 a。
// main.js
const a = require('./a');
console.log('in main, a.a1 = %j, a.a2 = %j', a.a1, a.a2);
// a.js
exports.a1 = true;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.a2 = true;
// b.js
const a = require('./a.js');
console.log('in b, a.a1 = %j, a.a2 = %j', a.a1, a.a2);
復(fù)制代碼
程序執(zhí)行結(jié)果如下:
in b, a.a1 = true, a.a2 = undefined
in main, a.a1 = true, a.a2 = true
復(fù)制代碼
實(shí)際上在模塊 a 代碼執(zhí)行之前就已經(jīng)創(chuàng)建了 Module 實(shí)例寫入了緩存,此時(shí)代碼還沒(méi)執(zhí)行,exports 是個(gè)空對(duì)象。
'/Users/evan/Desktop/module/a.js':
Module {
exports: {},
//...
}
}
代碼 exports.a1 = true; 修改了 module.exports 上的 a1 為 true, 這時(shí)候 a2 代碼還沒(méi)執(zhí)行。
'/Users/evan/Desktop/module/a.js':
Module {
exports: {
a1: true
}
//...
}
}
進(jìn)入 b 模塊,require a.js 時(shí)發(fā)現(xiàn)緩存上已經(jīng)存在了,獲取 a 模塊上的 exports 。打印 a1, a2 分別是 true,和 undefined。
運(yùn)行完 b 模塊,繼續(xù)執(zhí)行 a 模塊剩余的代碼,exports.a2 = true; 又往 exports 對(duì)象上增加了 a2 屬性,此時(shí) module a 的 export 對(duì)象 a1, a2 均為 true。
exports: {
a1: true,
a2: true
}
再回到 main 模塊,由于 require('./a.js') 得到的是 module a export 對(duì)象的引用,這時(shí)候打印 a1, a2 就都為 true。
這里還有一個(gè)需要注意的點(diǎn)就是,模塊在加載時(shí)是同步阻塞的,只有引入的模塊加載完才會(huì)執(zhí)行后面的語(yǔ)句,大家記住就好。
5.總結(jié):
說(shuō)了這么多我們主要的目的還是為了面試,所以這里小小的總結(jié)一下:
在CommonJS中一個(gè)文件就是一個(gè)模塊,模塊中的變量,方法,類都是私有的 module代表當(dāng)前模塊, module.exports代表模塊對(duì)外的接口模塊在加載時(shí)所有內(nèi)容會(huì)被放在一個(gè)立即執(zhí)行函數(shù)中,函數(shù)的this指向 module.exports這個(gè)空對(duì)象,而exports只是module.exports的引用而已加載模塊是同步阻塞的,加載后會(huì)進(jìn)行緩存,多次引入只會(huì)加載一次 require得到的模塊中變量,方法,類的拷貝,并不是直接的引用
ES6 module:
這個(gè)是我們最常用的,我們通常會(huì)在Vue或者Webpack中來(lái)使用,其并不像是CommonJS那樣將代碼放在一個(gè)立即執(zhí)行函數(shù)中(依靠閉包)從而完成模塊化,而是從語(yǔ)法層面完成的模塊化。一般情況下我們寫的ES6 module語(yǔ)法會(huì)還是會(huì)通過(guò)bable或者Webpack等工具轉(zhuǎn)化為CommonJS語(yǔ)法的。
對(duì)于ES6 module就不詳細(xì)介紹其實(shí)現(xiàn)原理了,主要想說(shuō)一下其特點(diǎn)并且和CommonJS相比有區(qū)別來(lái)方便大家記憶。
1.在執(zhí)行模塊前會(huì)先加載所有的依賴模塊
這點(diǎn)也是最重要的一點(diǎn),通過(guò)上面我們知道CommonJS是在執(zhí)行到需要加載依賴模塊時(shí),會(huì)(同步阻塞)停下當(dāng)前任務(wù)去加載相應(yīng)的依賴模塊,而對(duì)于ES module來(lái)說(shuō)無(wú)論你在哪一行引用依賴模塊,其都會(huì)在一開(kāi)始就進(jìn)行加載相應(yīng)的依賴模塊。
// a.mjs
export const a1 = true;
import * as b from './b.mjs';
export const a2 = true;
// b.mjs
import { a1, a2 } from './a.mjs'
console.log(a1, a2);
在這種情況下,如果是之前的CommonJS會(huì)輸出true與undefined,而現(xiàn)在會(huì)直接報(bào)錯(cuò):ReferenceError: Cannot access 'a1' before initialization。
同樣的原因我們?cè)贑ommonJS中可以這樣寫,而在ES module中會(huì)報(bào)錯(cuò):
require(path.join('xxxx', 'xxx.js'))

同樣如果我們?cè)贑ommonJS中引入一個(gè)沒(méi)有exports的變量那么在代碼執(zhí)行時(shí)才會(huì)報(bào)錯(cuò),而ES module在剛開(kāi)始就會(huì)報(bào)錯(cuò)。
2.import的是變量的引用
在CommonJS的情況下:
// counter.js
let count = 1;
function increment () {
count++;
}
module.exports = {
count,
increment
}
// main.js
const counter = require('counter.cjs');
counter.increment();
console.log(counter.count); // 1
在ES module情況下:
// counter.mjs
export let count = 1;
export function increment () {
count++;
}
// main.mjs
import { increment, count } from './counter.mjs'
increment();
console.log(count); // 2
這一次我們導(dǎo)入是變量的引用了,這樣可以避免之前CommonJS在實(shí)際開(kāi)發(fā)中的很多問(wèn)題,實(shí)際類似于這樣。
exports.counter = 1;
exports.increment = function () {
exports.counter++;
}
3.ES module是部分導(dǎo)入
這個(gè)很好理解,在CommonJS中我們加載一個(gè)模塊需要將該模塊的所有接口導(dǎo)入進(jìn)來(lái),而ES6 module里我們可以按需只導(dǎo)入我們想要的接口。
最后順便再提一點(diǎn):出于兼容性考慮對(duì)于像Webpack我們?cè)谑褂玫?code style="font-size: 14px;overflow-wrap: break-word;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;color: rgb(19, 148, 216);padding: 2px 6px;word-break: normal;">ES module時(shí)最終還是會(huì)轉(zhuǎn)換為CommonJS規(guī)范,所以有些時(shí)候我們使用require時(shí)導(dǎo)入的并不是目標(biāo)值,我們往往需要加一個(gè).defult才行,這就是因?yàn)?code style="font-size: 14px;overflow-wrap: break-word;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;color: rgb(19, 148, 216);padding: 2px 6px;word-break: normal;">ES module的exports defult語(yǔ)法所造成的。
4.總結(jié):
其實(shí)ES6 module相對(duì)于CommonJS最大的區(qū)別就是兩點(diǎn):
在執(zhí)行模塊前首先需要加載所有的依賴模塊,如果加載有問(wèn)題直接報(bào)錯(cuò) ES6 module的模塊引入的都是變量,函數(shù),類的引用這是很有先進(jìn)性的
還有值得一提的就是ES6 module可以按需引入自己需要的接口,兩者也是具有相同點(diǎn)的就是都會(huì)對(duì)已經(jīng)引入的模塊進(jìn)行緩存,如果多次引入只會(huì)執(zhí)行一次。
參考:
FESKY:CommonJS 和 ES6 Module 究竟有什么區(qū)別?[1]
雨中前行:再次梳理AMD、CMD、CommonJS、ES6 Module的區(qū)別[2]
參考
CommonJS 和 ES6 Module 究竟有什么區(qū)別?: https://juejin.cn/post/6844904080955932680#heading-7
[2]再次梳理AMD、CMD、CommonJS、ES6 Module的區(qū)別: https://juejin.cn/post/6844903983987834888#heading-10
