ES6躬行記(7)——程式碼模組化
在ES6之前,由於ECMAScript不具備模組化管理的能力,因此往往需要藉助第三方類庫(例如遵守AMD規範的RequireJS或遵循CMD規範的SeaJS等)才能實現模組載入。而自從ES6引入了模組化標準後,就不需要再特地載入一次外部指令碼了。模組化的語法不僅讓JavaScript程式碼的組織變得更有條理,還包含封裝、按需匯出或匯入等實用功能,可輕鬆應對日益複雜和龐大的前端工程。但有一點要注意,模組中的程式碼預設執行在嚴格模式中。
一、匯出
一個模組就是一個獨立的JavaScript檔案,如果要讀取檔案內的變數、函式或類(ES6新增的概念),那麼必須先將它們用export關鍵字匯出,因為它們預設都是私有的。匯出的方式有多種,下面會依依列舉。
1)第一種
將export關鍵字放在變數、函式等宣告之前,常被稱為命名匯出(named export),如下所示。注意,命名匯出的變數或函式都需要有名稱,否則會丟擲語法錯誤。
export let name = "strick"; export function getName() { return "strick"; } export class people { getName() { return "strick"; } }
以上述程式碼中的name變數為例,本質上,export匯出的是變數本身的引用,也就是為該變數在兩個模組之間建立一種關聯(即繫結)。如果變數在模組內部實時更新了,那麼匯出的變數的值也會隨之改變。
2)第二種
第二種也叫命名匯出,只是形式不同,宣告和匯出會分成兩步,要匯出的識別符號會用花括號包裹起來,如下程式碼所示。此時,還能通過as關鍵字為匯出的變數、函式等設定別名。
let age = 28; function getAge() { return 28; } export { age, getAge }; export { age as myAge, getAge as getMyAge };//設定別名
3)第三種
第三種用於匯出模組的全部或部分成員(例如變數、函式或類等),此時需要包含四部分,分別是匯出識別符號、模組路徑(也叫模組說明符,module specifier)以及兩個關鍵字:export和from。如果要匯出全部,那麼匯出識別符號得用星號(*)表示;而如果只要匯出部分,那麼匯出識別符號可以像第二種命名匯出那麼寫,具體如下程式碼所示。注意,模組路徑不能簡寫,需要以“/”、“./”或“../”開頭,千萬不要因為檔案在同級就省略相應的字元。
export * from "./1.js"; //匯出全部 export { name, age } from "./1.js";//匯出部分 export { getAge as getMyAge } from "./1.js";//匯出部分並設定別名
上面程式碼的第二條匯出語句,其實可以分解成下面兩條語句,第三條也有類似的分解。
import { name, age } from "./1.js"; export { name, age };
二、匯入
如果想匯入某個模組的成員,可以使用import關鍵字。它的語法與前面第三種匯出方式類似,也包含四個部分,分別是匯入識別符號、模組路徑以及兩個關鍵字:import和from,其中模組路徑也不能簡寫,如下程式碼所示。
import * as people from "./1.js";//匯入全部 import { name, age } from "./1.js";//匯入部分 import { getAge as getMyAge } from "./1.js";//匯入部分並設定別名
注意上面的第一行程式碼,使用了名稱空間匯入(Namespace Import)。與匯出模組的全部成員不同,在匯入時,除了要與星號組合之外,還必須為其設定別名。這是由於載入的整個模組會被當成一個物件,而此物件需要一個名稱,它的屬性就是該模組所有的匯出。另外兩行使用了命名匯入(Named Import),花括號內的匯入識別符號要與模組的匯出識別符號一一對應。
import語句在內部實現了單例模式,儘管上面程式碼對同一個模組執行了三次匯入,但該模組只會被例項化一次。
1)只讀變數
用import匯入的變數都是隻讀的,相當於為它添加了const限制,如果在模組中為其重新賦值,那麼必會引起型別錯誤。想要更新匯入的變數的值,有一種間接的實現辦法,如下程式碼所示,先在要匯出的1.js模組內定義name變數和setName()函式。
export let name = "strick"; export function setName(str) { name = str; }
然後在另一個模組中匯入剛剛的兩個成員,接著將新的name值通過setName()函式傳入到1.js模組內部進行更新,如下程式碼所示,name變數最終輸出的結果正是那個新值。
import { name, setName } from "./1.js"; console.log(name);//"strick" setName("freedom"); console.log(name);//"freedom"
2)成員提升
從模組中匯入的成員預設都會被提升至當前模組作用域的頂部,類似於宣告提升。因此,像下面這樣的寫法都是正確的。
console.log(age); getAge(); import { age, getAge } from "./1.js";
3)簡潔匯入
import語句中的匯入識別符號和from關鍵字都是可選的,但要注意,只有當兩者一起省略時,語句才能被正確執行,如下所示。
import "./jquery.js";
由於import載入的模組都會被執行一次,因此可以用上面這種簡潔匯入來實現指令碼的預載入。而這些指令碼既可以是自己封裝的程式碼段,也可以是jQuery、Zepto等第三方類庫。
三、模組的預設值
ES6中的default關鍵字可指定模組的預設值(例如變數、函式或類等),即為模組指定預設的匯出和匯入。
1)預設匯出
一個模組只能存在一個預設匯出,下面會列出預設匯出的四種寫法,為了便於比較,將它們放在了一起。
let name = "strick"; export default name; //寫法一 export default function getName() {//寫法二 return "strick"; } export default function() {//寫法三 return "strick"; } export { name as default };//寫法四
export語句中的default其實就是要匯出的模組成員,它的名稱就叫default,而default後面能夠跟一個表示式、命名函式或匿名函式,注意觀察上面程式碼的前三種寫法。第四種寫法比較特殊,是在命名匯出時,將識別符號重新命名成default。
預設匯出可以簡單的理解為給default賦值,因此下面的前兩條語句都能被正確執行,而第三條語句由於包含了宣告變數的關鍵字(let),所以會引起語法錯誤。
export default name = "strick"; export default "strick"; export default let name = "strick";//語法錯誤
2)預設匯入
如果要匯入模組的預設值,那麼可以像下面這樣寫,同樣,為了便於比較,將它們放在了一起。
import name from "./1.js"; //寫法一 import name, { age } from "./1.js";//寫法二 import name, * as people from "./1.js";//寫法三 import { default as myName } from "./1.js";//寫法四
因為模組只能有一個預設匯出,所以對應的匯入識別符號可以不用花括號包裹(注意觀察前三種寫法),不僅如此,還能像第四種寫法那樣通過default關鍵字為其重新命名。但有一點要注意,當同時使用預設和非預設的匯入識別符號時,必須把預設的寫在前面。
四、限制
1)模組路徑
由於ES6中的模組被設計成了靜態的,因此需要在編譯階段就明確模組之間的依賴關係,而不是在執行過程中動態計算,像下面這樣將模組路徑設為變數或表示式都是錯誤的寫法。
let path = "./1.js"; export * from path; //變數 export * from "./" + "1.js"; //表示式 import * as people from path; //變數 import * as people from "./" + "1.js";//表示式
2)作用域
export和import語句都是靜態的,無法動態匯出和匯入。因此只能出現在模組的頂層作用域中,而不能出現在塊級或函式作用域中,下面的寫法都會引起語法錯誤。
//函式作用域 function getName() { export * from "./1.js"; import * as people from "./1.js"; } //塊級作用域 if(true) { export * from "./1.js"; import * as people from "./1.js"; }
3)識別符號
匯出和匯入語句中的識別符號如果重複,那麼也會引起語法錯誤,如下所示。
export { name, name } from "./1.js"; import { name, name } from "./1.js";
五、用<script>標籤載入模組
在瀏覽器中,無論是以外部還是內聯的方式嵌入模組檔案,都需要將它的type屬性設為“module”,如下程式碼所示。並且在載入模組時為了避免指令碼阻塞,會自動應用布林屬性defer,即HTML文件的解析和模組檔案的下載是同時進行的,待到解析完後才會執行模組。
<script src="1.js" type="module"></script> <script type="module"> import { name } from "./1.js"; console.log(name); </script> <script src="2.js" type="module"></script>
上面程式碼會被依次執行,先執行第一個外部模組,再執行內聯模組,最後執行第二個外部模組。注意,在每個模組中用import匯入的其它模組也會被解析和下載,並且同一個模組每次只能被載入一次。