javascript中的require、import和export
為什麼有模組概念
理想情況下,開發者只需要實現核心的業務邏輯,其他都可以載入別人已經寫好的模組。
但是,Javascript不是一種模組化程式語言,在es6以前,它是不支援”類”(class),所以也就沒有”模組”(module)了。
require時代
Javascript社群做了很多努力,在現有的執行環境中,實現”模組”的效果。
原始寫法
模組就是實現特定功能的一組方法。
只要把不同的函式(以及記錄狀態的變數)簡單地放在一起,就算是一個模組。
1 2 3 4 5 6 | function m1(){ //... } function m2(){ //... } |
上面的函式m1()和m2(),組成一個模組。使用的時候,直接呼叫就行了。
這種做法的缺點很明顯:”汙染”了全域性變數,無法保證不與其他模組發生變數名衝突,而且模組成員之間看不出直接關係。
物件寫法
為了解決上面的缺點,可以把模組寫成一個物件,所有的模組成員都放到這個物件裡面
1 2 3 4 5 6 7 8 9 | var module1 = new Object({ _count : 0, m1 : function (){ //... }, m2 : function (){ //... } }); |
上面的函式m1()和m2(),都封裝在module1物件裡。使用的時候,就是呼叫這個物件的屬性
1 | module 1.m1(); |
這樣的寫法會暴露所有模組成員,內部狀態可以被外部改寫。比如,外部程式碼可以直接改變內部計數器的值。
1 | module._ count = 1; |
立即執行函式寫法
使用”立即執行函式”(Immediately-Invoked Function Expression,IIFE),可以達到不暴露私有成員的目的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var module = (function() { var _count = 0; var m1 = function() { alert(_count) } var m2 = function() { alert(_count + 1) } return { m1: m1, m2: m2 } })() |
使用上面的寫法,外部程式碼無法讀取內部的_count變數。
1 | console.info(module._count); //undefined |
module就是Javascript模組的基本寫法。
主流模組規範
在es6以前,還沒有提出一套官方的規範,從社群和框架推廣程度而言,目前通行的javascript模組規範有兩種:CommonJS 和 AMD
CommonJS規範
2009年,美國程式設計師Ryan Dahl創造了node.js專案,將javascript語言用於伺服器端程式設計。
這標誌”Javascript模組化程式設計”正式誕生。前端的複雜程度有限,沒有模組也是可以的,但是在伺服器端,一定要有模組,與作業系統和其他應用程式互動,否則根本沒法程式設計。
node程式設計中最重要的思想之一就是模組,而正是這個思想,讓JavaScript的大規模工程成為可能。模組化程式設計在js界流行,也是基於此,隨後在瀏覽器端,requirejs和seajs之類的工具包也出現了,可以說在對應規範下,require統治了ES6之前的所有模組化程式設計,即使現在,在ES6 module被完全實現之前,還是這樣。
在CommonJS中,暴露模組使用module.exports和exports,很多人不明白暴露物件為什麼會有兩個,後面會介紹區別
在CommonJS中,有一個全域性性方法require(),用於載入模組。假定有一個數學模組math.js,就可以像下面這樣載入。
1 | var math = require('math'); |
然後,就可以呼叫模組提供的方法:
1 2 | var math = require('math'); math.add( 2,3); // 5 |
正是由於CommonJS 使用的require方式的推動,才有了後面的AMD、CMD 也採用的require方式來引用模組的風格
AMD規範
有了伺服器端模組以後,很自然地,大家就想要客戶端模組。而且最好兩者能夠相容,一個模組不用修改,在伺服器和瀏覽器都可以執行。
但是,由於一個重大的侷限,使得CommonJS規範不適用於瀏覽器環境。還是上一節的程式碼,如果在瀏覽器中執行,會有一個很大的問題
1 2 | var math = require('math'); math.add(2, 3); |
第二行math.add(2, 3),在第一行require(‘math’)之後執行,因此必須等math.js載入完成。也就是說,如果載入時間很長,整個應用就會停在那裡等。
這對伺服器端不是一個問題,因為所有的模組都存放在本地硬碟,可以同步載入完成,等待時間就是硬碟的讀取時間。但是,對於瀏覽器,這卻是一個大問題,因為模組都放在伺服器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於”假死”狀態。
因此,瀏覽器端的模組,不能採用”同步載入”(synchronous),只能採用”非同步載入”(asynchronous)。這就是AMD規範誕生的背景。
AMD是”Asynchronous Module Definition”的縮寫,意思就是”非同步模組定義”。它採用非同步方式載入模組,模組的載入不影響它後面語句的執行。所有依賴這個模組的語句,都定義在一個回撥函式中,等到載入完成之後,這個回撥函式才會執行。
模組必須採用特定的define()函式來定義。
1 | define(id?, dependencies?, factory) |
- id:字串,模組名稱(可選)
- dependencies: 是我們要載入的依賴模組(可選),使用相對路徑。,注意是陣列格式
- factory: 工廠方法,返回一個模組函式
如果一個模組不依賴其他模組,那麼可以直接定義在define()函式之中。
1 2 3 4 5 6 7 8 9 | // math.js define( function (){ var add = function (x,y){ return x+y; }; return { add: add }; }); |
如果這個模組還依賴其他模組,那麼define()函式的第一個引數,必須是一個數組,指明該模組的依賴性。
1 2 3 4 5 6 7 8 | define([ 'Lib'], function(Lib){ function foo(){ Lib.doSomething(); } return { foo : foo }; }); |
當require()函式載入上面這個模組的時候,就會先載入Lib.js檔案。
AMD也採用require()語句載入模組,但是不同於CommonJS,它要求兩個引數:
1 | require([module], callback); |
第一個引數[module],是一個數組,裡面的成員就是要載入的模組;第二個引數callback,則是載入成功之後的回撥函式。如果將前面的程式碼改寫成AMD形式,就是下面這樣:
1 2 3 | require(['math'], function (math) { math.add(2, 3); }); |
math.add()與math模組載入不是同步的,瀏覽器不會發生假死。所以很顯然,AMD比較適合瀏覽器環境。
目前,主要有兩個Javascript庫實現了AMD規範:require.js和curl.js。
CMD規範
CMD (Common Module Definition), 是seajs推崇的規範,CMD則是依賴就近,用的時候再require。它寫起來是這樣的:
1 2 3 4 | define( function(require, exports, module) { var clock = require('clock') ; clock.start() ; }) ; |
CMD與AMD一樣,也是採用特定的define()函式來定義,用require方式來引用模組
1 | define(id?, dependencies?, factory) |
- id:字串,模組名稱(可選)
- dependencies: 是我們要載入的依賴模組(可選),使用相對路徑。,注意是陣列格式
- factory: 工廠方法,返回一個模組函式
1 2 3 4 5 | define( 'hello', ['jquery'], function(require, exports, module) { // 模組程式碼 }); |
如果一個模組不依賴其他模組,那麼可以直接定義在define()函式之中。
1 2 3 | define( function(require, exports, module) { // 模組程式碼 }); |
注意:帶 id 和 dependencies 引數的 define 用法不屬於 CMD 規範,而屬於 Modules/Transport 規範。
CMD與AMD區別
AMD和CMD最大的區別是對依賴模組的執行時機處理不同,而不是載入的時機或者方式不同,二者皆為非同步載入模組。
AMD依賴前置,js可以方便知道依賴模組是誰,立即載入;
而CMD就近依賴,需要使用把模組變為字串解析一遍才知道依賴了那些模組,這也是很多人詬病CMD的一點,犧牲效能來帶來開發的便利性,實際上解析模組用的時間短到可以忽略。
現階段的標準
ES6標準釋出後,module成為標準,標準使用是以export指令匯出介面,以import引入模組,但是在我們一貫的node模組中,我們依然採用的是CommonJS規範,使用require引入模組,使用module.exports匯出介面。
export匯出模組
export語法宣告用於匯出函式、物件、指定檔案(或模組)的原始值。
注意:在node中使用的是exports,不要混淆了
export有兩種模組匯出方式:命名式匯出(名稱匯出)和預設匯出(定義式匯出),命名式匯出每個模組可以多個,而預設匯出每個模組僅一個。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 …; |
- name1… nameN-匯出的“識別符號”。匯出後,可以通過這個“識別符號”在另一個模組中使用import引用
- default-設定模組的預設匯出。設定後import不通過“識別符號”而直接引用預設匯入
- -繼承模組並匯出繼承模組所有的方法和屬性
- as-重新命名匯出“識別符號”
- from-從已經存在的模組、指令碼檔案…匯出
命名式匯出
模組可以通過export字首關鍵詞宣告匯出物件,匯出物件可以是多個。這些匯出物件用名稱進行區分,稱之為命名式匯出。
1 2 | export { myFunction }; // 匯出一個已定義的函式 export const foo = Math.sqrt(2); // 匯出一個常量 |
我們可以使用*和from關鍵字來實現的模組的繼承:
1 | export * from 'article'; |
模組匯出時,可以指定模組的匯出成員。匯出成員可以認為是類中的公有物件,而非匯出成員可以認為是類中的私有物件:
1 2 3 4 5 | var name = 'IT筆錄'; var domain = 'http://itbilu.com'; export {name, domain}; // 相當於匯出 {name:name,domain:domain} |
模組匯出時,我們可以使用as關鍵字對匯出成員進行重新命名:
1 2 3 4 | var name = 'IT筆錄'; var domain = 'http://itbilu.com'; export {name as siteName, domain}; |
注意,下面的語法有嚴重錯誤的情況:
1 2 3 4 5 | // 錯誤演示 export 1; // 絕對不可以 var a = 100; export a; |
export在匯出介面的時候,必須與模組內部的變數具有一一對應的關係。直接匯出1沒有任何意義,也不可能在import的時候有一個變數與之對應
export a
雖然看上去成立,但是a的值是一個數字,根本無法完成解構,因此必須寫成export {a}
的形式。即使a被賦值為一個function,也是不允許的。而且,大部分風格都建議,模組中最好在末尾用一個export匯出所有的介面,例如:
1 | export { fun as default,a,b,c}; |
預設匯出
預設匯出也被稱做定義式匯出。命名式匯出可以匯出多個值,但在在import引用時,也要使用相同的名稱來引用相應的值。而預設匯出每個匯出只有一個單一值,這個輸出可以是一個函式、類或其它型別的值,這樣在模組import匯入時也會很容易引用。
1 2 | export default function() {}; // 可以匯出一個函式 export default class(){}; // 也可以出一個類 |
命名式匯出與預設匯出
預設匯出可以理解為另一種形式的命名匯出,預設匯出可以認為是使用了default名稱的命名匯出。
下面兩種匯出方式是等價的:
1 2 3 4 | const D = 123; export default D; export { D as default }; |
export使用示例
使用名稱匯出一個模組時:
1 2 3 4 5 6 | // "my-module.js" 模組 export function cube(x) { return x * x * x; } const foo = Math.PI + Math.SQRT2; export { foo }; |
在另一個模組(指令碼檔案)中,我們可以像下面這樣引用:
1 2 3 | import { cube, foo } from 'my-module'; console.log(cube(3)); // 27 console.log(foo); // 4.555806215962888 |
使用預設匯出一個模組時:
1 2 3 4 | // "my-module.js"模組 export default function (x) { return x * x * x; } |
在另一個模組(指令碼檔案)中,我們可以像下面這樣引用,相對名稱匯出來說使用更為簡單:
1 2 3 | // 引用 "my-module.js"模組 import cube from 'my-module'; console.log(cube(3)); // 27 |
import引入模組
import語法宣告用於從已匯出的模組、指令碼中匯入函式、物件、指定檔案(或模組)的原始值。
import模組匯入與export模組匯出功能相對應,也存在兩種模組匯入方式:命名式匯入(名稱匯入)和預設匯入(定義式匯入)。
import的語法跟require不同,而且import必須放在檔案的最開始,且前面不允許有其他邏輯程式碼,這和其他所有程式語言風格一致。
1 2 3 4 5 6 7 8 9 | import defaultMember from "module-name"; import * as name from "module-name"; import { member } from "module-name"; import { member as alias } from "module-name"; import { member1 , member2 } from "module-name"; import { member1 , member2 as alias2 , [...] } from "module-name"; import defaultMember, { member [ , [...] ] } from "module-name"; import defaultMember, * as name from "module-name"; import "module-name"; |
- name-從將要匯入模組中收到的匯出值的名稱
- member, memberN-從匯出模組,匯入指定名稱的多個成員
- defaultMember-從匯出模組,匯入預設匯出成員
- alias, aliasN-別名,對指定匯入成員進行的重新命名
- module-name-要匯入的模組。是一個檔名
- as-重新命名匯入成員名稱(“識別符號”)
- from-從已經存在的模組、指令碼檔案等匯入
命名式匯入
我們可以通過指定名稱,就是將這些成員插入到當作用域中。匯出時,可以匯入單個成員或多個成員:
注意,花括號裡面的變數與export後面的變數一一對應
1 2 | import {myMember} from "my-module"; import {foo, bar} from "my-module"; |
通過*符號,我們可以匯入模組中的全部屬性和方法。當匯入模組全部匯出內容時,就是將匯出模組(’my-module.js’)所有的匯出繫結內容,插入到當前模組(’myModule’)的作用域中:
1 | import * as myModule from "my-module"; |
匯入模組物件時,也可以使用as對匯入成員重新命名,以方便在當前模組內使用:
1 | import {reallyReallyLongModuleMemberName as shortName} from "my-module"; |
匯入多個成員時,同樣可以使用別名:
1 | import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module"; |
匯入一個模組,但不進行任何繫結:
1 | import "my-module"; |
預設匯入
在模組匯出時,可能會存在預設匯出。同樣的,在匯入時可以使用import指令匯出這些預設值。
直接匯入預設值:
1 | import myDefault from "my-module"; |
也可以在名稱空間匯入和名稱匯入中,同時使用預設匯入:
1 2 3 4 | import myDefault, * as myModule from "my-module"; // myModule 做為名稱空間使用 或 import myDefault, {foo, bar} from "my-module"; // 指定成員匯入 |
import使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // --file.js-- function getJSON(url, callback) { let xhr = new XMLHttpRequest(); xhr.onload = function () { callback( this.responseText) }; xhr.open( "GET", url, true); xhr.send(); } export function getUsefulContents(url, callback) { getJSON(url, data => callback(JSON.parse(data))); } // --main.js-- import { getUsefulContents } from "file"; getUsefulContents( "http://itbilu.com", data => { doSomethingUseful(data); }); |
default關鍵字
1 2 3 4 5 6 | // d.js export default function() {} // 等效於: function a() {}; export {a as default}; |
在import的時候,可以這樣用:
1 2 3 4 | import a from './d'; // 等效於,或者說就是下面這種寫法的簡寫,是同一個意思 import {default as a} from './d'; |
這個語法糖的好處就是import的時候,可以省去花括號{}。
簡單的說,如果import的時候,你發現某個變數沒有花括號括起來(沒有*號),那麼你在腦海中應該把它還原成有花括號的as語法。
所以,下面這種寫法你也應該理解了吧:
1 | import $,{each,map} from 'jquery'; |
import後面第一個$是{defalut as $}的替代寫法。
as關鍵字
as簡單的說就是取一個別名,export中可以用,import中其實可以用:
1 2 3 4 5 6 7 | // 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這個介面:
1 2 | // c.js export function fun() {}; |
如果在b.js中