此文爲翻譯,原文地址在這兒:https://hacks.mozilla.org/2015/08/es6-in-depth-modules/【轉】javascript
ES6 是 ECMAScript 第 6 版本的簡稱,這是新一代的 JavaScript 的標準。ES6 in Depth 是關於 ES6 的一系列新特性的介紹。html
遙想 2007 年,筆者開始在 Mozilla 的 JavaScript 團隊工做的時候,那個時候典型的 JavaScript 程序只有一行代碼。java
兩年以後, Google Map 被髮布。可是在那以前不久,JavaScript 的主要用途仍是表單驗證,固然啦,你的<input onchange=>
處理器平均來講只有一行。webpack
事過情遷,JavaScript 項目已經變得十分龐大,社區也發展出了一些有助於開發可擴展程序的工具。首先你須要的即是模塊系統。模塊系統讓你得以將你的工做分散在不一樣的文件和目錄中,讓它們以前得以互相訪問,而且能夠很是有效地加載它們。天然而然地,JavaScript 發展出了模塊系統,事實上是多個模塊系統(AMD,CommonJS,CMD,譯者注)。不只如此,社區還提供了包管理工具(NPM,譯者注),讓你能夠安裝和拷貝高度依賴其餘模塊的軟件。也許你會以爲,帶有模塊特性的 ES6,來得有些晚了。git
一個 ES6 的模塊是一個包含了 JS 代碼的文件。ES6 裏沒有所謂的 module
關鍵字。一個模塊看起來就和一個普通的腳本文件同樣,除了如下兩個區別:es6
ES6 的模塊自動開啓嚴格模式,即便你沒有寫 'use strict'
。github
你能夠在模塊中使用 import
和 export
。web
讓咱們先來看看 export
。在模塊中聲明的任何東西都是默認私有的,若是你想對其餘模塊 Public,你必須 export
那部分代碼。咱們有幾種實現方法,最簡單的方式是添加一個 export
關鍵字。npm
// 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 模塊加載規範,並且咱們作出來了。它沒有成爲最終標準的緣由之一是它沒法與打包這一特性調和。模塊系統須要被標準化,打包也不該該被放棄,由於它太好了。
做爲一門動態編程語言,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 協同工做。
若是你今天就要開始使用,你須要諸如 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 系列的總結文章。