簡述JavaScript模組化程式設計(二)
前置閱讀:簡述JavaScript模組化(一)
在前面一文中,我們對前端模組化所經歷的三個階段進行了瞭解:
- CommonJs,由於是同步的,所以主要應用於伺服器端,以Node.js為代表。
- AMD,非同步模組定義,預載入,推薦依賴前置。以require.js為代表。
- CMD,通用模組載入,懶載入,推薦依賴就近。以Sea.js為代表。
而在ES6已經大行其道的今天,ES6中所提供的模組化的方法也自然而然成了我們進行JavaScript模組化程式設計的標準,因此ES6模組的語法雖然在一些較為老的瀏覽器上不能直出,需要進行轉譯,但卻代表著未來的JavaScript發展趨勢。
ES6模組特性
在ES6中將模組認為是自動執行在嚴格模式下並且沒有辦法退出執行的JavaScript程式碼。在一個模組中定義的變數不會自動被新增到全域性共享的作用域之中,這個變數只能作用在這個作用域中。此外模組還必須匯出一些外部檔案可以訪問的元素,以供其他模組或程式碼使用。
除了這個基本特性,ES6模組還有兩大特性也十分重要,需要額外注意:
- 首先是在模組的頂部this值是undefined,這是由於在ES6中的模組的程式碼是在嚴格模式下執行的。(如果對this不是很熟悉的可以去看我的這篇文章:深入淺出this關鍵字)
- 其次,模組不支援HTML風格的程式碼註釋,這是早期瀏覽器所遺留下的JavaScript特性,在ES6的語法裡不予支援。
基本用法-模組載入
首先我們來看瀏覽器是如何載入模組的。其實在ES6規範出來之前,web瀏覽器就規定了三種方式來引入JavaScript檔案:
- 在沒有src屬性的<script>元素中直接內嵌JavaScript程式碼
- 在<script>元素中通過src屬性指定一個地址來載入JavaScript程式碼檔案
- 通過Web Worker或Service Worker的方法載入並執行JavaScript程式碼
而在瀏覽器中,預設的行為就是將JavaScript作為指令碼來進行載入,而非模組。所以我們要告訴瀏覽器我們載入的是模組,方法就是在<script>元素中,將type屬性指定為"module"。具體看下面的示例:
1 // 第一種方式 2 <script type=""module> 3 import { add } from "./example"; 4 let num = add(1, 1); 5 </script> 6 // 第二種方式 7 <script type="module" src="example.js"> 8 // 第三種方式,以指令碼的方式載入example.js 9 let worker = new Worker("example.js");
當HTML解析器遇到<script>元素的type="module"的時候,模組檔案就開始下載,直到檔案被完全解析完成才會去執行模組內的程式碼。模組檔案是按照他們出現在HTML檔案中順序執行的,也就是說無論用何種方式引入模組,第一個<script type=""module>總是在第二個<script type=""module>之前執行。
基本用法-匯出
在ES6中我們可以使用export關鍵字將一部分程式碼暴露給其他模組,以供其他模組或程式碼使用。先讓我們來看看export關鍵字在MDN的定義吧:
export語句用於在建立JavaScript模組時,從模組中匯出函式、物件或原始值,以便其他程式可以通過 import 語句使用它們。(
此特性目前僅在 Safari 和 Chrome 原生實現。它在許多轉換器中實現,如Traceur Compiler,Babel或Rollup。)
通過MDN的定義我們可以知道:export關鍵字可以將其放在任何函式、物件或原始值前面,從而將它們從模組中匯出。示例如下:
1 // ./example.js 2 // 匯出變數 3 export var a = 1; 4 // 匯出函式 5 export function addA(value) { 6 return value + a; 7 } 8 //匯出類 9 export class add1 { 10 constructor(value) { 11 this.value = value + a; 12 } 13 } 14 //這個函式就是這個模組所私有的,在外部不能訪問它 15 function say1() { 16 console.log('我是不是很帥'); 17 } 18 //這又是個函式 19 function say2() { 20 console.log('沒錯我就是很帥'); 21 } 22 //在後面對函式進行匯出,它就不是私有的了 23 export say2;
需要注意的是:使用export匯出的函式和類都需要一個名稱,除非使用default關鍵字,否則就不能用這個方法匯出匿名函式或類。所以當我們需要匯出匿名的函式或者類時,我們可以這麼做:
1 // ./example.js 2 //匯出匿名函式 3 export default function(a, b) { 4 return a + b; 5 } 6 //或者匯出匿名的類 7 export default class { 8 consturctor(value) { 9 this.value = value + 1; 10 } 11 }
具體關於default關鍵字的用法我會在後面做具體介紹,現在只需記住:當我們需要匯出匿名的函式或者類時要使用export default語法。
基本語法-匯入
在ES6中,從模組中匯入的功能可以通過import關鍵字。import語句由兩部分組成:要匯入元素的識別符號和元素應當從哪個模組匯入。
1 // ./say.js 2 import { say2 } from "./example.js"; 3 console.log(say2()); // '沒錯我就是很帥'
import 後面的大括號中的say2表示從規定模組匯入的元素的名稱。關鍵字from後面的字串則表示要匯入的模組的路徑,這通常是包含模組的.js檔案的相對或絕對路徑名,需要注意的是隻允許使用單引號和雙引號的字串來包裹路徑,瀏覽器使用的路徑格式與傳給<script>元素的相同,所以必須把檔案的副檔名也加上。
(注:由於Node.js遵循基於檔案系統字首以區分本地檔案個包的慣例,即example是一個包,而./exampple.js是一個本地檔案。為了更好的相容多個瀏覽器Node.js環境,我們一定要在路徑前包含./或../來表示要匯入的檔案。)
除此之外,我們還可以匯入多個元素或者直接匯入整個模組:
1 // 匯入多個元素 2 improt { a, addA, say2 } from "./example.js"; 3 console.log(a); // 1 4 sonsole.log(addA(1); // 2 5 6 // 匯入整個模組 7 import * as example from "./example.js" 8 console.log(example.a); // 1 9 sonsole.log(example.addA(1); // 2 10 console.log(example.say2()); // '沒錯我就是很帥'
上面的匯入整個模組就是把example.js中匯出的所有元素全部載入到一個叫做example的物件中,而所匯出的元素就會作為example的屬性被訪問。因為example物件是作為example.js中所匯出成員的名稱空間物件而被建立的,所以這種匯入方式被稱為名稱空間匯入(name space import)。
還有一點要注意的是,不管import語句把一個模組寫了多少次,該模組只執行一次。意思就是,在首次執行匯入模組後,例項化的模組就會被儲存在記憶體中,只要使用import語句引用它就可以重複使用它:
1 // 首次匯入需要載入模組example.js 2 import { a } from "./example.js" 3 // 下面的兩個import將無需載入example.js了 4 import { addA } from "./example.js" 5 import { say2 } from "./example.js"
當從模組中匯入一個元素時,它與const是一樣無法定義另一個同名變數和匯入一個同名元素,也無法在import語句前使用元素或者改變匯出的元素的值:
1 //接上面的程式碼 2 3 say2 = 1 ; //會丟擲一個錯誤
這是由於ES6的import語句為匯入的元素建立的是隻讀繫結的識別符號,而不是原始繫結。因此元素只有在被匯出的模組中才可以被修改,即使是將該模組的全部匯入也無法修改其中的元素。
1 // ./example.js 2 // 這是一個函式 3 export function setA(newA) { 4 a = newA; 5 } 6 // ./say.js 7 import { a, setA } from "./example"; 8 console.log(a); // 1 9 a = 2; //丟擲錯誤 10 11 // 所以我們得這麼做 12 setA(2); 13 console.log(a); // 2
呼叫setA(2)時會返回到example.js中去執行,將a設定為2。由於say.js匯入的只是a的只讀繫結的識別符號而已,因此會自動進行更改。
其他基本語法
1.語法限制
export和import在語法上還有一個重要的限制,那就是他們必須在條件語句和函式之外使用,例如:
1 if (ture) { 2 export var a = 1; //語法錯誤 3 } 4 function imp() { 5 import a from "./example.js"; //語法錯誤 6 }
由於模組語法存在的其中一個原因是讓JavaScript引擎可以靜態地確定哪些程式碼是可以匯出的,因此export和import語句被設計成靜態的,不能進行任何形式的動態匯出或匯入。
2.重新命名解決
有時在開發中,我們在匯入一些元素後不想使用它們的原始名稱了,我們就可以在匯出過程或者匯入過程中去改變匯出元素的名稱:
1 // 匯出過程 2 function add(a, b) { 3 return a + b; 4 } 5 export { add as add1 }; //在匯入過程中必須使用add1作為名稱 6 7 // 匯入過程 8 import {add as add1 } from "./example" 9 console.log(add1(1,1)); // 2 10 console.log(typeof add); //undefined
3.模組的預設值
在CommonJS等其他的模組化規範中,從模組中匯出或匯入預設值是一個常見的用法,因此在ES6中也延用了這種用法並進行了優化。在ES6中我們可以使用default關鍵字來指定預設值,並且一個模組只能預設一個匯出值:
1 // ./example.js 2 // 第一種預設匯出語法 3 export default function(a, b) { 4 return a + b; 5 } 6 // 第二種預設匯出語法 7 function add(a, b) { 8 return a + b; 9 } 10 export default add; 11 // 第三種預設匯出語法 12 function add(a, b) { 13 return a + b; 14 } 15 export { add as default };
需要注意的是第三種語法,default關鍵字雖然不能作為元素的名稱,但可以作為元素的屬性名稱,因此可以使用as語法將add函式的屬性設定為default。
匯入預設值的語法則是這樣的:
1 // 第一種語法 2 import add from "./example"; 3 // 第二種語法 4 import { default as add } from "./example";
看到這裡有些朋友可能會發現,我們的第一種語法中import關鍵字後面並沒有加大括號,認為這是錯誤的。其實這是匯入預設值的獨特語法,在這的本地名稱add用於表示模組匯出的任何預設函式,這種語法是最純淨的,ES6標準建立團隊的大佬們也希望這種語法能成為web主流的模組匯入形式。
我們前面說的匯入匿名函式也同樣使用這種語法:
1 // ./example.js 2 //匯出匿名函式 3 export default function(a, b) { 4 return a + b; 5 } 6 // ./say.js 7 import add from "./example"; 8 console.log(add(1,1)); // 2
在這裡本地名稱add就是用於表示上面的匿名函式的。
4.匯出已匯入的元素
我們同樣可以在本模組內匯出我們在本模組內匯入的元素,有以下幾種語法:
1 // 第一種語法 2 import { add } from ./example.js; 3 export { add }; 4 5 // 第二種語法 6 export { add } from ./example.js; 7 8 //換一個名稱匯出 9 export { add as add1 } from ./example.js; //以add這個名稱匯入,再以add1的名稱匯出 10 11 // 匯出整個模組 12 export * from ./example.js;
// 最後預祝自己8月份面試成功offer滿天飛-_-||