前置閱讀:簡述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滿天飛-_-||