天天看點

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導入的其它子產品也會被解析和下載下傳,并且同一個子產品每次隻能被加載一次。