回想2007年,那時候我剛加入Mozilla's JavaScript團隊,那時候的一個典型的JavaScript程式只需要一行程式碼,聽起來像個笑話。
兩年後,Google Maps釋出。在這之前,JavaScript主要用來做表單的驗證,你用來處理<input onchange=>
這個程式當然只需要一行。
時過境遷,JavaScript專案已經發展到讓人歎為觀止,社群湧現了許多幫助開發的工具。但是最迫切需要的是一個模組系統,它能將你的工作分散到不同的檔案與目錄中,在需要的時候他們能彼此之間相互訪問,並且可以有效的載入所有程式碼。所以JavaScript有模組系統這很正常,而且還有多個模組系統(CommonJS、AMD、CMD、UMD)。不僅如此,它還有幾個包管理器(npm、bower),用來安裝軟體還能拷貝一些深度依賴。你可能認為ES6和它的新模組系統出現得有點晚。
那我們來看看ES6為現存的模組系統新增了什麼,以及未來的標準和工具能否建立在這個系統上。首先,讓我們看看ES6的模組是什麼樣子的。
模組的基礎知識
ES6模組是一個包含了JS程式碼的檔案。沒有所謂的module
關鍵詞,一個模組看起來和一個指令碼檔案沒什麼不一樣,除了一下兩個區別:
- ES6的模組自動開啟嚴格模式,即使你沒有寫
"use strict";
; - 在模組中,你可以使用
import
和exprot
。
先來談談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() {
...
}
...複製程式碼
你可以export
任何的頂級變數:function
、class
、var
、let
、const
。
你如果要寫一個模組知道這麼多就夠了!你不必再把所有的東西放到一個立即執行函式或者回撥函式裡面,只需要在你需要的地方進行宣告。由於這個程式碼是一個模組,而不是一個指令碼,所有的宣告的作用域都只屬於這個模組,而不是所有指令碼和模組都能全域性訪問的。你只要把模組中的宣告匯出成一組公共模組的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 { ... }複製程式碼
匯出列表並不需要寫在檔案的第一行,它可以出現在模組檔案的頂級作用域的任何位置。你可以有多個匯出列表,或者將匯出列表與匯出宣告混合使用,只要不重複匯出同一個變數名就行。
重新命名匯出和匯入
有時,匯入的變數名碰巧與你需要使用的一些變數名衝突了,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
而不是each
,你依然想像以前一樣使用它。或者, 你想把_
當成一個函式來使用,因為這才是Lodash。
這種情況下,你只要稍微改變下你的寫法:不使用花括號來匯入模組。
import _ from "lodash";複製程式碼
這個寫法等同於import {default as _} from "lodash";
。所有的CommonJS 和AMD模組在ES6中都能被當作default
匯出,這個匯出和你在CommonJS中使用require()
匯出得到東西一樣,即exports
物件。
ES6模組在設計上可以讓你匯出更多的東西,但對於現在的CommonJS模組,匯出的default模組就是能匯出的全部東西了。例如,在寫這篇文章時,據我所知,著名的colors模組沒有特意去支援ES6語法,這是一個CommonJS模組組成的包,就像npm上的那些包一樣,但是你可以直接引入到你的ES6程式碼中。
// ES6 equivalent of `var colors = require("colors/safe");`
import colors from "colors/safe";複製程式碼
如果你希望自己ES6模組也具有預設匯出,這很簡單。預設的匯出方式並沒有什麼魔力;他就像其他匯出一樣,除了它的匯出名為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()
來進行呼叫。
聚合模組
有時候一個包的主模組只不過是匯入包其他所有的模組,並用統一的方式匯出。為了簡化這種程式碼,有一種將匯入匯出全部合一的簡寫:
// 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
表示式類似於import-from
後面跟了一個export
。這和真正的匯入有一些區別,它不會在當前作用域中繫結將要匯出的變數。如果你打算在world-foods.js
中使用Tea
來編寫一些程式碼,請不要使用這種簡寫,你會發現Tea為定義。
如果“singapore”匯出的命名與其他匯出發生了衝突,那就會出現錯誤,所以請謹慎使用。
呼,我們已經把語法介紹完了!下面來談談一些有趣的事情。
import
到底做了什麼?
不管你信不信,它什麼都沒做。
噢,你看起來沒那麼好騙。那麼你會相信標準幾乎沒有說import
到底該怎麼做嗎?這是件好事嗎?(作者貌似很愛開玩笑。)
ES6將模組的載入細節完全交給了實現,其餘的模組執行部分卻規定得非常詳細。
簡單來說,當你告訴JS引擎執行一個模組的時候,它的行為可以歸納為以下四部:
- 解析:讀取模組的原始碼,並檢查語法錯誤。
- 載入:載入所有的匯入模組(遞迴進行),這是還未標準化的部分。
- 連結:對於每個新載入的模組,在實現上都會建立一個作用域,並把模組中宣告的所有變數都繫結在這個作用域上,包括從其他模組匯入的變數。
如果你想試試import {cake} from "paleo"
,但是“paleo”模組沒真正匯出名為cake的變數,你會得到一個錯誤。這很糟糕,因為你離執行js並品嚐蛋糕只有一步之遙。 - 執行時間:最後,開始執行載入進來的新的模組中的程式碼。這時,整個
import
過程已經完成了,所以前面說程式碼執行到import
這一行宣告時,什麼都沒有發生。
看到沒?我說了什麼都不會發生,在程式語言這件事上,我從來都不說慌。
現在我們可以開始介紹這個系統中有趣的部分了。這有一個非常炫酷的技巧。由於系統沒有指定如何載入的這方面的細節,並且你可以通過檢視原始碼中匯入的宣告,提前計算出所有的依賴項,所以ES6的實現可以通過前處理器完成所有的工作,然後把所有的模組打包到一個檔案中,最後通過網路進行請求一次即可。像webpack這樣的工具就是這麼做的。
這是一個優雅的解決方案,因為通過網路載入所有的指令碼檔案很耗時,假如你請求一個資源後,發現裡面有import
宣告,然後你又得請求更多資源。一個載入器需要非常多的網路請求來回傳輸。通過webpack,你不僅能在今天就使用ES6的模組話,你還能獲得很多好處,並且不需要擔心會造成執行時的效能下降。
原本是有計劃制定一個ES6中模組載入的詳細規範的,並且已經初步成型。它沒有成為標準的原因之一是不知道如何與打包這一特性進行整合。我希望模組化的載入會更加標準化,也希望打包工具會越來越好。
靜態 VS 動態,或者說:規則如何打破規則
作為一個動態編譯語言,令人驚奇的是JavaScript擁有一個靜態的模組系統。
- 所有的
import
和export
只能寫在頂級作用域中。你不能在條件判斷語句和函式作用域內使用import
。 - 所有匯出的變數名必須是顯式的,你不能通過遍歷一個陣列,動態生成一組匯出名進行匯出。
- 模組物件都是被凍結的,不能通過polyfill為它新增新的特性。
- 在所有模組執行之前, 其依賴的模組都必須經過載入、解析、連結的過程,目前沒有
import
懶載入相關的語法。(現在import()方法已經在提案中了) - 對於
import
的錯誤,無法進行recovery。一個應用可能依賴許多的模組,一旦有一個模組載入失敗,這個應用都不會執行。你不能在try/catch
中使用import
。正是因為es6的模組表現得如此靜態,webpack才能在編譯的時候檢測出程式碼中的錯誤。 - 你沒法為一個模組在載入所有依賴項之前新增鉤子,這意味著一個模組沒有辦法控制其依賴項的載入方式。
如果你的需求是靜態的,ES6的模組系統還是相當不錯的。但是你有時候你還是向進行一些hack,對吧?
這就是為什麼你使用的模組載入系統會提供一些系統層次的API來配合ES6的靜態的import/export
語法。例如,webpack有一個API能進行程式碼的分割,按照你的需求對一些模組進行懶載入。這個API能夠打破之前列出的規矩。
ES6的模組語法非常靜態,這很好-在使用一些編譯工具時我們都能嚐到一些甜頭。
靜態語法的設計可以讓它與動態載入器豐富的API進行工作。
我什麼時候才能使用ES6模組?
如果你今天就想使用,你需要一個預編譯器,如 Traceur 和 Babel 。這個系列之前也有相關文章,Gastón I. Silva:如何使用 Babel 和 Broccoli 編譯 ES6 程式碼為 web 可用。Gastón也將案例放在了 GitHub 上。另外這篇文章也介紹瞭如何使用 Babel 和 webpack。
ES6 模組系統由 Dave Herman 和 Sam Tobin-Hochstadt進行設計,他們不顧多人(包括我)的反對,多年來始終堅持模組系統是靜態的。JonCoppeard正在Firefox上實現ES6的模組化功能。JavaScript Loader的相關標準的工作也正在進行中,預計在HTML中將會被新增類似<script type=module>
這樣的東西。
這便是 ES6 了。
這太有趣了,我不希望現在就結束。也許我們還能再說一會。我們還能夠討論一些關於ES6規範中零零碎碎的東西,但這些又不足夠寫成文章。也行會有一些關於ES6未來特性的一些東西,盡請期待下週的ES6 In Depth