探討ES6的import export default 和CommonJS的require module.exports

今天來(lái)扒一扒在node和ES6中的module,主要是為了區(qū)分node和ES6中的不同意義,避免概念上的混淆,同時(shí)也分享一下,自己在這個(gè)坑里獲得的心得。

在ES6之前

模塊的概念是在ES6發(fā)布之前就出現(xiàn)的,我感覺(jué)主要是為了適應(yīng)大型應(yīng)用開發(fā)的需要而引入了JavaScript世界。模塊化編程已經(jīng)從噱頭上升為必備,所以ES6也順應(yīng)時(shí)代,把這個(gè)寫進(jìn)了標(biāo)準(zhǔn)。

CommonJS和AMD都是JavaScript模塊化規(guī)范,在ES6之前,Node主要遵循CommonJS,而AMD則主要運(yùn)用在瀏覽器端,比如requirejs。

而且node發(fā)布的時(shí)候,就天生具備module,所以從某種意義上講,是node促進(jìn)了js世界里面的模塊化編程。

// module-file.js for node
module.exports = {
  a : function() {},
  b : 'xxx'
};

把上面這個(gè)js文件放在node環(huán)境中,我們這樣去用它:

var myModule = require('./module-file');
var b = myModule.b;myModule.a();

從模塊化思想出發(fā),requirejs和國(guó)內(nèi)前端大牛發(fā)布的seajs(遵循CMD)也允許前端猿們通過(guò)require去加載另一個(gè)模塊。不過(guò)在模塊定義的時(shí)候,需要借助一個(gè)define函數(shù):

// module-file.js for requirejs
define(function(require, exports, module){
   module.exports = {};
});

requirejs和seajs都支持上面這種形式,在define的回調(diào)函數(shù)中提供了模擬的require, exports, module,這讓習(xí)慣了node環(huán)境中使用方法的猿類可以順便在瀏覽器端寫基本相同的代碼。

node,requirejs,seajs也同時(shí)支持下面這種導(dǎo)出模塊的方式:

define(function(){
  return {}; // return的值就是導(dǎo)出的模塊
});

這就出現(xiàn)了UMD,即一個(gè)兼容多種環(huán)境的方案。

Node,requirejs中的exports

node中導(dǎo)出模塊接口就是用exports,你可以這樣做:

module.exports.a = function() {};
module.exports.b = 'xxx';

也可以寫在一個(gè)對(duì)象中:

module.exports = {
  a : function() {},
  b : 'xxx'
}

在requirejs中,還提供了一個(gè)exports變量作為module.exports的別名:

define(function(require, exports, module){
   exports.a = function(){};
   exports.b = 'xxx';
});

注意“別名”的含義:exports是module.exports的地址的引用。它的本質(zhì)是:

var exrpots = module.exports;

因此,你必須注意兩個(gè)點(diǎn),就是導(dǎo)出接口的時(shí)候:
1.不能直接用exports={}來(lái)導(dǎo)出整個(gè)接口;
2.如果使用了module.exports={},那么exports.a等都會(huì)被覆蓋無(wú)效。

ES6中的import和export

在ES6之前,要使用一個(gè)模塊,必須使用require函數(shù)將一個(gè)模塊引入,但ES6并沒(méi)有采用這種模塊化方案,在ES6中使用import指令引入一個(gè)模塊或模塊中的部分接口,并沒(méi)有將require寫入標(biāo)準(zhǔn),這也就是說(shuō)require對(duì)于ES6代碼而言,只是一個(gè)普通函數(shù)。

同理,在ES6標(biāo)準(zhǔn)中,導(dǎo)出模塊的接口也只能使用export指令,而非exports對(duì)象,這也同樣意味著module.exports只是node,requirejs等模塊化庫(kù)的自定義變量,而非ES標(biāo)準(zhǔn)接口。

常見(jiàn)export方式

在ES6規(guī)定中,這樣導(dǎo)出一個(gè)模塊的接口:

export function fun() {};
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // also var
export let name1 = …, name2 = …, …, nameN; // also var, const
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
export * from …;export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;

ES6里面,直接把要導(dǎo)出的變量、函數(shù)、對(duì)象、類等前面加一個(gè)export關(guān)鍵字。比如:

// module-file.js
export function a(){};
export var obj = {};

有一個(gè)點(diǎn):export必須導(dǎo)出具有對(duì)應(yīng)關(guān)系的變量,下面的接口輸出是錯(cuò)誤的:

// 錯(cuò)誤演示
export 1; // 這種導(dǎo)出的內(nèi)容不是變量是絕對(duì)錯(cuò)誤的,包括導(dǎo)出表達(dá)式,也是絕對(duì)錯(cuò)誤的
var a = 1;
export a;
function b() {}
export b;

上面的這些方法都是錯(cuò)誤的,不能這樣導(dǎo)出接口。導(dǎo)出接口僅限兩種:

1.聲明時(shí)導(dǎo)出
2.以對(duì)象的形式導(dǎo)出(和解構(gòu)聯(lián)系起來(lái))

如果要導(dǎo)出某個(gè)變量,可以用花括號(hào)括起來(lái),像這樣:

var a = 1;
export {a}; // 等效于:{a:a}
function b() {}
export {b};

我有點(diǎn)疑惑的是,為何export允許多次export {}這種形式?看上去很奇怪。另外,ES6更厲害之處在于,可以在export變量之后,繼續(xù)修改變量:

export var obj {};obj.a = 1;

在import之后,obj的值仍然可以在模塊內(nèi)繼續(xù)改變,這是CommonJS以往不可能做到的。

import基本用法

在另外一個(gè)js文件里面這樣使用這些接口:

import {a,obj} from './module-file';
a();
alert(obj.b);

和node之前的require不一樣,require只能把模塊放到一個(gè)變量中,而在ES6中,擁有對(duì)象解構(gòu)賦值的能力,所以直接就把引入的模塊的接口賦值給變量了。內(nèi)在機(jī)理也有不同,require需要去執(zhí)行整個(gè)模塊,將整個(gè)模塊放到內(nèi)存中(也就是我們說(shuō)的運(yùn)行時(shí)),如果只是使用到其中一個(gè)方法,性能上就差很多,而import...from則是只加載需要的接口方法,其他方法在程序啟動(dòng)之后根本觸及不到,所以這種又被稱為“編譯時(shí)”,性能上好很多。

as關(guān)鍵字

編程的同學(xué)對(duì)as都容易理解,簡(jiǎn)單的說(shuō)就是取一個(gè)別名。上面export中可以用,import中其實(shí)也可以用:

// a.js
var a = function() {};
export {a as fun};
// b.js
import {fun as a} from './a';a();

上面這段代碼,export的時(shí)候,對(duì)外提供的接口是fun,它是a.js內(nèi)部a這個(gè)函數(shù)的別名,但是在模塊外面,認(rèn)不到a,只能認(rèn)到fun。

import中的as就很簡(jiǎn)單,就是你在使用模塊里面的方法的時(shí)候,給這個(gè)方法取一個(gè)別名,好在當(dāng)前的文件里面使用。之所以是這樣,是因?yàn)橛械臅r(shí)候不同的兩個(gè)模塊可能通過(guò)相同的接口,比如有一個(gè)c.js也通過(guò)了fun這個(gè)接口:

// c.js
export function fun() {};

如果在b.js中同時(shí)使用a和c這兩個(gè)模塊,就必須想辦法解決接口重名的問(wèn)題,as就解決了。

default關(guān)鍵字

其他人寫教程什么的,都把default放到export那個(gè)部分,我覺(jué)得不利于理解。在export的時(shí)候,可能會(huì)用到default,說(shuō)白了,它其實(shí)是別名的語(yǔ)法糖:

// d.js
export default function() {}
// 等效于:function a() {}; export {a as default};

在import的時(shí)候,可以這樣用:

import a from './d';
// 等效于,或者說(shuō)就是下面這種寫法的簡(jiǎn)寫,是同一個(gè)意思import {default as a} from './d';

這個(gè)語(yǔ)法糖的好處就是import的時(shí)候,可以省去花括號(hào){}。簡(jiǎn)單的說(shuō),如果import的時(shí)候,你發(fā)現(xiàn)某個(gè)變量沒(méi)有花括號(hào)括起來(lái),那么你在腦海中應(yīng)該把它還原成有花括號(hào)的as語(yǔ)法。

所以,下面這種寫法你也應(yīng)該理解了吧:

import _,{each,map} from '_';

*符號(hào)

*就是代表所有,只用在import中,我們看下兩個(gè)例子:

import * as underscore from '_';

在意義上和import _ from '';是不同的,雖然實(shí)際上后面的使用方法是一樣的。它表示的是把''模塊中的所有接口掛載到underscore這個(gè)對(duì)象上,所以可以用underscore.each調(diào)用某個(gè)接口。

export * from '_';
// 等效于:import * as all from '_';export all;

該用require還是import?

接下來(lái)的問(wèn)題,就是我們?cè)趯?shí)操中,還有必要用require嗎?我感覺(jué)ES6標(biāo)準(zhǔn)已經(jīng)將之前的所有模塊化規(guī)范都給碾壓了,這在以前的標(biāo)準(zhǔn)發(fā)布中極少見(jiàn),ES6用更加簡(jiǎn)單的方式,實(shí)現(xiàn)了更加有效的module,感覺(jué)require可以回家養(yǎng)老了。

標(biāo)準(zhǔn)與非標(biāo)準(zhǔn)

既然是標(biāo)準(zhǔn),那么就是所有引擎應(yīng)該去實(shí)現(xiàn)的,node和瀏覽器未來(lái)都會(huì)直接支持這種模塊加載方式,require完成歷史使命回家自己玩兒。而且作為node或?yàn)g覽器,同時(shí)可以利用import提供自己的API,比如手機(jī)端提供基于網(wǎng)絡(luò)的定位API,這都不用SDK了,直接內(nèi)置在客戶端內(nèi)部,import一下就可以了。

不過(guò)現(xiàn)在import導(dǎo)入模塊還并不是全部環(huán)境都支持,使用babel可以讓node支持ES6,但在瀏覽器端,則毫無(wú)辦法,可能還得暫時(shí)依賴require。但是非常不好的消息是,require不是ES6標(biāo)準(zhǔn),這也就是說(shuō)如果將來(lái)瀏覽器支持import后,你想用它,就必須升級(jí)代碼,而不能直接被兼容。

import只能在文件開頭使用,在import之前,你不能有其他的代碼,這和其他語(yǔ)言是一樣的。但是require則不同,它相當(dāng)于node的一個(gè)定義在全局的函數(shù),你可以在任意地方使用它,甚至使用變量表達(dá)式作為它的參數(shù),這樣有一個(gè)好處,就是可以在循環(huán)中加載模塊。

有沒(méi)有兼容import和require的模塊?

但是很坑的是,node的模塊導(dǎo)出和ES6標(biāo)準(zhǔn)也不符,因?yàn)閚ode的模塊體系遵循的是CommonJS規(guī)范,這就導(dǎo)致你寫的模塊文件,不可能同時(shí)支持require和import。

要強(qiáng)調(diào)的就是,不要把require和import兩種模塊加載方案混用,比如:

// module-file.js
module.exports = {};
// a.js
import a from './moule-file';

這種混搭感覺(jué)不是很好(但可以用,下面有解釋)。所以,其實(shí)我沒(méi)有任何建議,我只是覺(jué)得,躺在坑里,挺自在的……畢竟node中require的使用更加靈活一點(diǎn),它沒(méi)有必須放在哪里的限制,所以可以在任意位置使用,而且它的結(jié)果也非常形象,甚至可以把require當(dāng)做一個(gè)引用類型別名,可以這樣使用:

require('./a')(); // a模塊是一個(gè)函數(shù),立即執(zhí)行a模塊函數(shù)
var data = require('./a').data; // a模塊導(dǎo)出的是一個(gè)對(duì)象
var a = require('./a')[0]; // a模塊導(dǎo)出的是一個(gè)數(shù)組

這樣的寫法感覺(jué)像給模塊取了一個(gè)別名,使用的時(shí)候非常靈活。但是需要注意的是,如果你打算使用require來(lái)導(dǎo)入這個(gè)模塊,那么請(qǐng)使用module.exports導(dǎo)出這個(gè)模塊。

(臨時(shí))兼容方案

有沒(méi)有一種兼容方案呢?

function a() {}
class b {}
module.exports = {a,b}; // {a,b}是ES6的寫法

在實(shí)踐中發(fā)現(xiàn),module.exports可以兼容require和import,而且這個(gè)案例需要你的node環(huán)境配置好支持ES6語(yǔ)法。module.exports導(dǎo)出的模塊,如果使用import,那么完全就是一個(gè)對(duì)象賦值、解構(gòu)的過(guò)程:

import mod,{a,b} from './a';

之所以這是成立的,是因?yàn)槲覀兪褂胋abel對(duì)ES6代碼進(jìn)行轉(zhuǎn)碼后執(zhí)行,而實(shí)際上,目前為止,沒(méi)有任何一個(gè)環(huán)境是支持ES6 module方案的,即使babel,也僅僅是將ES6的import,export轉(zhuǎn)碼為require, module.exports后交給node去執(zhí)行。

導(dǎo)出的模塊接口被賦值給mod,所以mod是一個(gè)對(duì)象,含有a,b兩個(gè)方法。這里的mod并沒(méi)有通過(guò)default導(dǎo)出,所以和ES6有非常大的意義上的區(qū)別,這種非標(biāo)準(zhǔn)的寫法,墻裂建議永遠(yuǎn)不要用。而且,由于require和module.exports是非標(biāo)準(zhǔn)的東西,僅在Node環(huán)境中有效,所以當(dāng)未來(lái)瀏覽器支持模塊導(dǎo)入時(shí),并不會(huì)主動(dòng)提供require,而是采用import,如果要使用require,還是不得不使用requirejs等庫(kù),借助define來(lái)用。

所以,最終,如果你打算用CommonJS,就不要摻和進(jìn)ES6.

轉(zhuǎn)載鏈接:https://www.tangshuang.net/2882.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容