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

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

同樣如果我們在CommonJS中引入一個沒有exports的變量那么在代碼執(zhí)行時才會報錯,而ES module在剛開始就會報錯。
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
這一次我們導入是變量的引用了,這樣可以避免之前CommonJS在實際開發(fā)中的很多問題,實際類似于這樣。
exports.counter = 1;
exports.increment = function () {
exports.counter++;
}
3.ES module是部分導入
這個很好理解,在CommonJS中我們加載一個模塊需要將該模塊的所有接口導入進來,而ES6 module里我們可以按需只導入我們想要的接口。
最后順便再提一點:出于兼容性考慮對于像Webpack我們在使用的ES module時最終還是會轉(zhuǎn)換為CommonJS規(guī)范,所以有些時候我們使用require時導入的并不是目標值,我們往往需要加一個.defult才行,這就是因為ES module的exports defult語法所造成的。
4.總結(jié):
其實ES6 module相對于CommonJS最大的區(qū)別就是兩點:
在執(zhí)行模塊前首先需要加載所有的依賴模塊,如果加載有問題直接報錯 ES6 module的模塊引入的都是變量,函數(shù),類的引用這是很有先進性的
還有值得一提的就是ES6 module可以按需引入自己需要的接口,兩者也是具有相同點的就是都會對已經(jīng)引入的模塊進行緩存,如果多次引入只會執(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
