此文為翻譯,原文地址在這兒:https://hacks.mozilla.org/2015/08/es6-in-depth-modules/
ES6 是 ECMAScript 第 6 版本的簡稱,這是新一代的 JavaScript 的標準。ES6 in Depth 是關於 ES6 的一系列新特性的介紹。
遙想 2007 年,筆者開始在 Mozilla 的 JavaScript 團隊工作的時候,那個時候典型的 JavaScript 程式只有一行程式碼。
兩年之後, Google Map 被髮布。但是在那之前不久,JavaScript 的主要用途還是表單驗證,當然啦,你的<input onchange=>
處理器平均來說只有一行。
事過情遷,JavaScript 專案已經變得十分龐大,社群也發展出了一些有助於開發可擴充套件程式的工具。首先你需要的便是模組系統。模組系統讓你得以將你的工作分散在不同的檔案和目錄中,讓它們之前得以互相訪問,並且可以非常有效地載入它們。自然而然地,JavaScript 發展出了模組系統,事實上是多個模組系統(AMD,CommonJS,CMD,譯者注)。不僅如此,社群還提供了包管理工具(NPM,譯者注),讓你可以安裝和拷貝高度依賴其他模組的軟體。也許你會覺得,帶有模組特性的 ES6,來得有些晚了。
模組基礎
一個 ES6 的模組是一個包含了 JS 程式碼的檔案。ES6 裡沒有所謂的 module
關鍵字。一個模組看起來就和一個普通的指令碼檔案一樣,除了以下兩個區別:
-
ES6 的模組自動開啟嚴格模式,即使你沒有寫
`use strict`
。 -
你可以在模組中使用
import
和export
。
讓我們先來看看 export
。在模組中宣告的任何東西都是預設私有的,如果你想對其他模組 Public,你必須 export
那部分程式碼。我們有幾種實現方法,最簡單的方式是新增一個 export
關鍵字。
// kittydar.js - Find the locations of all the cats in an image.
// (Heather Arthur wrote this library for real)
// (but she didn`t use modules, because it was 2013)
export function detectCats(canvas, options) {
var kittydar = new Kittydar(options);
return kittydar.detectCats(canvas);
}
export class Kittydar {
... several methods doing image processing ...
}
// This helper function isn`t exported.
function resizeCanvas() {
...
}
...
你可以在 function
、class
、var
、let
或 const
前新增 export
。
如果你想寫一個模組,有這些就夠了!再也不用把程式碼放在 IIFE 或者一個回撥函式裡了。既然你的程式碼是一個模組,而非指令碼檔案,那麼你生命的一切都會被封裝進模組的作用域,不再會有跨模組或跨檔案的全域性變數。你匯出的宣告部分則會成為這個模組的 Public API。
除此之外,模組裡的程式碼和普通程式碼沒啥大區別。它可以訪問一些基本的全域性變數,比如 Object
和 Array
。如果你的模組跑在瀏覽器裡,它將可以訪問 document
和 XMLHttpRequest
。
在另外一個檔案中,我們可以匯入這個模組並且使用 detectCats()
函式:
// demo.js - Kittydar demo program
import {detectCats} from "kittydar.js";
function go() {
var canvas = document.getElementById("catpix");
var cats = detectCats(canvas);
drawRectangles(canvas, cats);
}
要匯入多個模組中的介面,你可以這樣寫:
import {detectCats, Kittydar} from "kittydar.js";
當你執行一個包含 import
宣告的模組,被引入的模組會先被匯入並載入,然後根據依賴關係,每一個模組的內容會使用深度優先的原則進行遍歷。跳過已經執行過的模組,以此避免依賴迴圈。
這便是模組的基礎部分,挺簡單的。
匯出表
如果你覺得在每個要匯出的部分前都寫上 export
很麻煩,你可以只寫一行你想要匯出的變數列表,再用花括號包起來。
export {detectCats, Kittydar};
// no `export` keyword required here
function detectCats(canvas, options) { ... }
class Kittydar { ... }
匯出表不一定要出現在檔案的第一行,它可以出現在模組頂級作用域中的任何一行。你可以寫多個匯出表,也可以在列表中再寫上其他 export
宣告,只要沒有變數名被重複匯出即可。
重名命匯出和匯入
如果匯入的變數名恰好和你模組中的變數名衝突了,ES6 允許你給你匯入的東西重新命名:
// suburbia.js
// Both these modules export something named `flip`.
// To import them both, we must rename at least one.
import {flip as flipOmelet} from "eggs.js";
import {flip as flipHouse} from "real-estate.js";
...
類似地,你在匯出變數的時候也能重新命名。這個特性在你想將同一個變數名匯出兩次的場景下十分方便,舉個例子:
// unlicensed_nuclear_accelerator.js - media streaming without drm
// (not a real library, but maybe it should be)
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
預設匯出
新一代的標準的設計理念是相容現有的 CommonJS
和 AMD
模組。所以如果你有一個 Node 專案,並且剛剛執行完 npm install lodash
,你的 ES6 程式碼可以獨立引入 Lodash 中的函式:
import {each, map} from "lodash";
each([3, 2, 1], x => console.log(x));
然而如果你已經習慣了 _.each
或者看不見 _
的話就渾身難受,當然這樣使用 Lodash 也是不錯的方式。
這種情況下,你可以稍微改變一下你的 import 寫法,不寫花括號:
import _ from "lodash";
這個簡寫等價於 import {default as _} from "lodash";
。所有 CommonJS 和 AMD 模組在被 ES6 程式碼使用的時候都已經有了預設的匯出,這個匯出和你在 CommonJS 中 require()
得到的東西是一樣的,那就是 exports
物件。
ES6 的模組系統被設計成讓你可以一次性引入多個變數。但對於已經存在的 CommonJS 模組來說,你能得到的只有預設匯出。舉個例子,在撰寫此文之時,據筆者所知,著名的 colors 模組並未特意支援 ES6。這是一個由多個 CommonJS 模組組成的模組,正如 npm 上的那些包。然而你依然可以直接將其引入到你的 ES6 程式碼中。
// ES6 equivalent of `var colors = require("colors/safe");`
import colors from "colors/safe";
如果你想寫自己的預設匯出,那也很簡單。這裡面並沒有什麼高科技,它和普通的匯出沒什麼兩樣,除了它的匯出名是 default
。你可以使用我們之前已經介紹過的語法:
let myObject = {
field1: value1,
field2: value2
};
export {myObject as default};
這樣更好:
export default {
field1: value1,
field2: value2
};
export default
關鍵字後可以跟隨任何值:函式,物件,物件字面量,任何你能說得出的東西。
模組物件
抱歉,這篇文章的內容有點多,但 JavaScript 已經算好的了:因為一些原因,所有語言的模組系統都有一大堆沒什麼卵用的特性。所幸的是,我們們只有一個話題要討論了,呃,好吧,兩個。
import * as cows from "cows";
當你 import *
,被引入進來的是一個 module namespace object
。它的屬性是那個模組的匯出,所以如果 “cows” 模組匯出了一個名為 moo()
的函式,當你像這樣引入了 “cows” 之後,你可以這樣寫 cows.moo()
。
聚合模組
有時候一個包的主模組會引入許多其他模組,然後再將它們以一個統一的方式匯出。為了簡化這樣的程式碼,我們有一個 import-and-export 的簡寫方法:
// world-foods.js - good stuff from all over
// import "sri-lanka" and re-export some of its exports
export {Tea, Cinnamon} from "sri-lanka";
// import "equatorial-guinea" and re-export some of its exports
export {Coffee, Cocoa} from "equatorial-guinea";
// import "singapore" and export ALL of its exports
export * from "singapore";
這種 export-from
的表示式和後面跟了一個 export
的 import-from
表示式類似。但和真正的匯入不同,它並不會在你的作用域中加入二次匯出的變數繫結。所以如果你打算在 world-foods.js
寫用到了 Tea
的程式碼,就別使用這個簡寫形式。
如果 “singapore” 匯出的某一個變數恰巧和其他的匯出變數名衝突了,那麼這裡就會出現一個錯誤。所以你應該謹慎使用 export *
。
Whew!我們介紹完語法了,接下來進入有趣的環節。
import
到底幹了啥
啥也沒幹,信不信由你。
噢,你好像看起來沒那麼好騙。好吧,那你相信標準幾乎沒有談到 import
該做什麼嗎?你認為這是一件好事還是壞事呢?
ES6 將模組的載入細節完全交給了實現,其餘的執行部分則規定得非常詳細。
大致來說,當 JS 引擎執行一個模組的時候,它的行為大致可歸納為以下四步:
-
解析:引擎實現會閱讀模組的原始碼,並且檢查是否有語法錯誤。
-
載入:引擎實現會(遞迴地)載入所有被引入的模組。這部分我們還沒標準化。
-
連結:引擎實現會為每個新載入的模組建立一個作用域,並且將模組中的宣告繫結填入其中,包括從其他模組中引入的。
當你嘗試 import {cake} from "paleo"
但是 “paleo” 模組並沒有匯出叫 cake
的東西時候,你也會在此時得到錯誤。這很糟糕,因為你離執行 JS,品嚐 cake
只差一步了!
-
執行:終於,JS 引擎開始執行剛載入進來的模組中的程式碼。到這個時候,
import
的處理過程已經完成,因此當 JS 引擎執行到一行import
宣告的時候,它啥也不會幹。
看到了不?我說了 import “啥也沒幹”,沒騙你吧?有關程式語言的嚴肅話題,哥從不說謊。
不過,現在我們們可以介紹這個體系中有趣的部分了,這是一個非常酷的 trick。正因為這個體系並沒有指定載入的細節,也因為你只需要看一眼原始碼中的 import
宣告就可以在執行前搞清楚模組的依賴,某些 ES6 的實現甚至可以通過預處理就完成所有的工作,然後將模組全部打包成一個檔案,最後通過網路分發。像 webpack 這樣的工具就是做這個事情的。
這非常的了不起,因為通過網路載入資源是非常耗時的。假設你請求一個資源,接著發現裡面有 import
宣告,然後你又得請求更多的資源,這又會耗費更多的時間。一個 naive 的 loader 實現可能會發起許多次網路請求。但有了 webpack,你不僅可以在今天就開始使用 ES6,還可以得到一切模組化的好處並且不向執行時效能妥協。
原先我們計劃過一個詳細定義的 ES6 模組載入規範,而且我們做出來了。它沒有成為最終標準的原因之一是它無法與打包這一特性調和。模組系統需要被標準化,打包也不應該被放棄,因為它太好了。
動態 VS 靜態,或者說:規矩和如何打破規矩
作為一門動態程式語言,JavaScript 令人驚訝地擁有一個靜態的模組系統。
-
import
和export
只能寫在頂級作用域中。你無法在條件語句中使用引入和匯出,你也不能在你寫的函式作用域中使用import
。 -
所有的匯出必須顯示地指定一個變數名,你也無法通過一個迴圈動態地引入一堆變數。
-
模組物件被封裝起來了,我們無法通過 polyfill 去 hack 一個新 feature。
-
在模組程式碼執行之前,所有的模組都必須經歷載入,解析,連結的過程。沒有可以延遲載入,惰性
import
的語法。 -
對於
import
錯誤,你無法在執行時進行 recovery。一個應用可能包含了幾百個模組,其中的任何一個載入失敗或連結失敗,這個應用就不會執行。你無法在try/catch
語句中import
。(不過正因為 ES6 的模組系統是如此地靜態,webpack 可以在預處理時就為你檢測出這些錯誤)。 -
你沒辦法 hook 一個模組,然後在它被載入之前執行你的一些程式碼。這意味著模組無法控制它的依賴是如何被載入的。
只要你的需求都是靜態的話,這個模組系統還是很 nice 的。但你還是想 hack 一下,是嗎?
這就是為啥你使用的模組載入系統可能會提供 API。舉個例子,webpack 有一個 API,允許你 “code splitting”,按照你的需求去惰性載入模組。這個 API 也能幫你打破上面列出的所有規矩。
ES6 的模組是非常靜態的,這很好——許多強大的編譯器工具因此收益。而且,靜態的語法已經被設計成可以和動態的,可程式設計的 loader API 協同工作。
我何時能開始使用 ES6 模組?
如果你今天就要開始使用,你需要諸如 Traceur 和 Babel 這樣的預處理工具。這個系列專題之前也有文章介紹了如何使用 Babel 和 Broccoli 去生成可用於 Web 的 ES6 程式碼。那篇文章的栗子也被開源在了 GitHub 上。筆者的這篇文章也介紹瞭如何使用 Babel 和 webpack。
ES6 模組系統的主要設計者是 Dave Herman 和 Sam Tobin-Hochstadt,此二人不顧包括筆者在內的數位委員的反對,始終堅持如今你見到的 ES6 模組系統的靜態部分,爭論長達數年。Jon Coppeard 正在火狐瀏覽器上實現 ES6 的模組。之後包括 JavaScript Loader 規範在內的工作已經在進行中。HTML 中類似 <script type=module>
這樣的東西之後也會和大家見面。
這便是 ES6 了。
歡迎大家對 ES6 進行吐槽,請期待下週 ES6 in Depth 系列的總結文章。