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

BothEyes1993發表於2019-02-02

今天來扒一扒在node和ES6中的module,主要是為了區分node和ES6中的不同意義,避免概念上的混淆,同時也分享一下,自己在這個坑裡獲得的心得。

在ES6之前

模組的概念是在ES6釋出之前就出現的,我感覺主要是為了適應大型應用開發的需要而引入了JavaScript世界。模組化程式設計已經從噱頭上升為必備,所以ES6也順應時代,把這個寫進了標準。

CommonJS和AMD都是JavaScript模組化規範,在ES6之前,Node主要遵循CommonJS,而AMD則主要運用在瀏覽器端,比如requirejs。

而且node釋出的時候,就天生具備module,所以從某種意義上講,是node促進了js世界裡面的模組化程式設計。

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

把上面這個js檔案放在node環境中,我們這樣去用它:

var myModule = require('./module-file');
var b = myModule.b;myModule.a();
複製程式碼

從模組化思想出發,requirejs和國內前端大牛釋出的seajs(遵循CMD)也允許前端猿們通過require去載入另一個模組。不過在模組定義的時候,需要藉助一個define函式:

// module-file.js for requirejs
define(function(require, exports, module){
   module.exports = {};
});
複製程式碼

requirejs和seajs都支援上面這種形式,在define的回撥函式中提供了模擬的require, exports, module,這讓習慣了node環境中使用方法的猿類可以順便在瀏覽器端寫基本相同的程式碼。

node,requirejs,seajs也同時支援下面這種匯出模組的方式:

define(function(){
  return {}; // return的值就是匯出的模組
});
複製程式碼

這就出現了UMD,即一個相容多種環境的方案。

Node,requirejs中的exports

node中匯出模組介面就是用exports,你可以這樣做:

module.exports.a = function() {};
module.exports.b = 'xxx';
複製程式碼

也可以寫在一個物件中:

module.exports = {
  a : function() {},
  b : 'xxx'
}
複製程式碼

在requirejs中,還提供了一個exports變數作為module.exports的別名:

define(function(require, exports, module){
   exports.a = function(){};
   exports.b = 'xxx';
});
複製程式碼

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

var exrpots = module.exports;
複製程式碼

因此,你必須注意兩個點,就是匯出介面的時候: 1.不能直接用exports={}來匯出整個介面; 2.如果使用了module.exports={},那麼exports.a等都會被覆蓋無效。

ES6中的import和export

在ES6之前,要使用一個模組,必須使用require函式將一個模組引入,但ES6並沒有採用這種模組化方案,在ES6中使用import指令引入一個模組或模組中的部分介面,並沒有將require寫入標準,這也就是說require對於ES6程式碼而言,只是一個普通函式。

同理,在ES6標準中,匯出模組的介面也只能使用export指令,而非exports物件,這也同樣意味著module.exports只是node,requirejs等模組化庫的自定義變數,而非ES標準介面。

常見export方式

在ES6規定中,這樣匯出一個模組的介面:

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裡面,直接把要匯出的變數、函式、物件、類等前面加一個export關鍵字。比如:

// module-file.js
export function a(){};
export var obj = {};
複製程式碼

有一個點:export必須匯出具有對應關係的變數,下面的介面輸出是錯誤的:

// 錯誤演示
export 1; // 這種匯出的內容不是變數是絕對錯誤的,包括匯出表示式,也是絕對錯誤的
var a = 1;
export a;
function b() {}
export b;
複製程式碼

上面的這些方法都是錯誤的,不能這樣匯出介面。匯出介面僅限兩種:

1.宣告時匯出 2.以物件的形式匯出(和解構聯絡起來)

如果要匯出某個變數,可以用花括號括起來,像這樣:

var a = 1;
export {a}; // 等效於:{a:a}
function b() {}
export {b};
複製程式碼

我有點疑惑的是,為何export允許多次export {}這種形式?看上去很奇怪。另外,ES6更厲害之處在於,可以在export變數之後,繼續修改變數:

export var obj {};obj.a = 1;
複製程式碼

在import之後,obj的值仍然可以在模組內繼續改變,這是CommonJS以往不可能做到的。

import基本用法

在另外一個js檔案裡面這樣使用這些介面:

import {a,obj} from './module-file';
a();
alert(obj.b);
複製程式碼

和node之前的require不一樣,require只能把模組放到一個變數中,而在ES6中,擁有物件解構賦值的能力,所以直接就把引入的模組的介面賦值給變數了。內在機理也有不同,require需要去執行整個模組,將整個模組放到記憶體中(也就是我們說的執行時),如果只是使用到其中一個方法,效能上就差很多,而import...from則是隻載入需要的介面方法,其他方法在程式啟動之後根本觸及不到,所以這種又被稱為“編譯時”,效能上好很多。

as關鍵字

程式設計的同學對as都容易理解,簡單的說就是取一個別名。上面export中可以用,import中其實也可以用:

// a.js
var a = function() {};
export {a as fun};
// b.js
import {fun as a} from './a';a();
複製程式碼

上面這段程式碼,export的時候,對外提供的介面是fun,它是a.js內部a這個函式的別名,但是在模組外面,認不到a,只能認到fun。

import中的as就很簡單,就是你在使用模組裡面的方法的時候,給這個方法取一個別名,好在當前的檔案裡面使用。之所以是這樣,是因為有的時候不同的兩個模組可能通過相同的介面,比如有一個c.js也通過了fun這個介面:

// c.js
export function fun() {};
複製程式碼

如果在b.js中同時使用a和c這兩個模組,就必須想辦法解決介面重名的問題,as就解決了。

default關鍵字

其他人寫教程什麼的,都把default放到export那個部分,我覺得不利於理解。在export的時候,可能會用到default,說白了,它其實是別名的語法糖:

// d.js
export default function() {}
// 等效於:function a() {}; export {a as default};
複製程式碼

在import的時候,可以這樣用:

import a from './d';
// 等效於,或者說就是下面這種寫法的簡寫,是同一個意思import {default as a} from './d';
複製程式碼

這個語法糖的好處就是import的時候,可以省去花括號{}。簡單的說,如果import的時候,你發現某個變數沒有花括號括起來,那麼你在腦海中應該把它還原成有花括號的as語法。

所以,下面這種寫法你也應該理解了吧:

import _,{each,map} from '_';
複製程式碼

*符號

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

import * as underscore from '_';
複製程式碼

在意義上和import _ from '';是不同的,雖然實際上後面的使用方法是一樣的。它表示的是把''模組中的所有介面掛載到underscore這個物件上,所以可以用underscore.each呼叫某個介面。

export * from '_';
// 等效於:import * as all from '_';export all;
複製程式碼

該用require還是import?

接下來的問題,就是我們在實操中,還有必要用require嗎?我感覺ES6標準已經將之前的所有模組化規範都給碾壓了,這在以前的標準釋出中極少見,ES6用更加簡單的方式,實現了更加有效的module,感覺require可以回家養老了。

標準與非標準

既然是標準,那麼就是所有引擎應該去實現的,node和瀏覽器未來都會直接支援這種模組載入方式,require完成歷史使命回家自己玩兒。而且作為node或瀏覽器,同時可以利用import提供自己的API,比如手機端提供基於網路的定位API,這都不用SDK了,直接內建在客戶端內部,import一下就可以了。

不過現在import匯入模組還並不是全部環境都支援,使用babel可以讓node支援ES6,但在瀏覽器端,則毫無辦法,可能還得暫時依賴require。但是非常不好的訊息是,require不是ES6標準,這也就是說如果將來瀏覽器支援import後,你想用它,就必須升級程式碼,而不能直接被相容。

import只能在檔案開頭使用,在import之前,你不能有其他的程式碼,這和其他語言是一樣的。但是require則不同,它相當於node的一個定義在全域性的函式,你可以在任意地方使用它,甚至使用變數表示式作為它的引數,這樣有一個好處,就是可以在迴圈中載入模組。

有沒有相容import和require的模組?

但是很坑的是,node的模組匯出和ES6標準也不符,因為node的模組體系遵循的是CommonJS規範,這就導致你寫的模組檔案,不可能同時支援require和import。

要強調的就是,不要把require和import兩種模組載入方案混用,比如:

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

複製程式碼

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

require('./a')(); // a模組是一個函式,立即執行a模組函式
var data = require('./a').data; // a模組匯出的是一個物件
var a = require('./a')[0]; // a模組匯出的是一個陣列
複製程式碼

這樣的寫法感覺像給模組取了一個別名,使用的時候非常靈活。但是需要注意的是,如果你打算使用require來匯入這個模組,那麼請使用module.exports匯出這個模組。

(臨時)相容方案

有沒有一種相容方案呢?

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

在實踐中發現,module.exports可以相容require和import,而且這個案例需要你的node環境配置好支援ES6語法。module.exports匯出的模組,如果使用import,那麼完全就是一個物件賦值、解構的過程:

import mod,{a,b} from './a';
複製程式碼

之所以這是成立的,是因為我們使用babel對ES6程式碼進行轉碼後執行,而實際上,目前為止,沒有任何一個環境是支援ES6 module方案的,即使babel,也僅僅是將ES6的import,export轉碼為require, module.exports後交給node去執行。

匯出的模組介面被賦值給mod,所以mod是一個物件,含有a,b兩個方法。這裡的mod並沒有通過default匯出,所以和ES6有非常大的意義上的區別,這種非標準的寫法,牆裂建議永遠不要用。而且,由於require和module.exports是非標準的東西,僅在Node環境中有效,所以當未來瀏覽器支援模組匯入時,並不會主動提供require,而是採用import,如果要使用require,還是不得不使用requirejs等庫,藉助define來用。

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

轉載連結:www.tangshuang.net/2882.html

相關文章