【译】《Understanding ECMAScript6》- 第八章-Module
目錄
- 模塊是什么
- 使用基礎
- 接口標識符重命名
- 缺省接口
- Re-exporting
- 非綁定import
- 總結
JavaScript令人困惑并且易引發錯誤的特性之一是以“一切皆共享”的方式加載代碼。所有文件內定義的一切代碼都共享一個全局作用域,這一點是JavaScript落后于其他編程語言之處(比如Java中的package)。隨著web應用變得越來越龐大復雜,“一切皆共享”的方式暴露出一系列弊端,比如命名沖突、安全性等等。ES6的目標之一便是解決這種問題,增強JavaScript代碼組織的有序性。這就是Module(模塊)的作用。
module是什么
Module可以簡單理解為加載JavaScript文件的一種特殊方式。目前,不論是瀏覽器還是NodeJS,都沒有實現原生ES6 Module的支持,但是我們可以期待Module作為一種默認的機制被廣泛使用。模塊化的代碼與非模塊的代碼有以下區別:
模塊化JavaScript文件和常規的文件相同,都是通過文本編輯器撰寫,使用.js擴展名。唯一的區別是,模塊化代碼使用全新的代碼語法。
使用基礎
export關鍵字用來導出一個模塊暴露給外部的代碼。最簡單的一種使用方式是在任何變量、函數、class聲明語句的前面使用export。如下:
// export data export var color = "red"; export let name = "Nicholas"; export const magicNumber = 7;// export function export function sum(num1, num2) {return num1 + num1; }// export class export class Rectangle {constructor(length, width) {this.length = length;this.width = width;} }// this function is private to the module function subtract(num1, num2) {return num1 - num2; }// define a function function multiply(num1, num2) {return num1 * num2; }// export later export multiply;需要注意以下幾點:
使用export的一個重要限制是,必須在當前模塊的最頂層作用域使用,否則會拋出語法錯誤。如下:
if (flag) {export flag; // syntax error }上述代碼中,export在if塊級域內使用會拋出語法錯誤。export不能以任何動態的方式導出,這樣做的好處是可以令JavaScript引擎對導出的模塊進行清晰地管理。因此,export只能在一個模塊的最頂層作用域內使用。
某些轉譯器(如Babel.js)可以打破這種限制,開發者可以在任何位置使用export。但是這種模式只在代碼被轉譯為ES5規范時能夠正常工作,并不支持原生的ES6模塊系統。
一旦使用export導出某個模塊的功能,便可以在其他模塊中通過import關鍵字使用它。import語句包括兩部分:被導入的標識符和此標識符的源模塊。如下:
import { identifier1, identifier2 } from "module";花括號內的標識符代表的是從指定模塊中導出的變量。關鍵字from后的模塊名代表的是被導出變量的指定模塊。模塊名是一個字符串。截止到本書撰寫日期,模塊名的書寫規范仍然未最終定稿。
盡管import后的花括號形式與解構Object類似,但它只是導出標識符的列表,并不是解構Object。
使用import從模塊中導出的變量類似于使用const定義的常量。也就是說,在同一作用域內,不能定義與之同名的變量,不能在import之前使用它,也不能重新賦值。
本章第一個例子中的模塊我們命名為“example”,你可以使用多種方式導出example模塊的標識符,最簡單的方式如下:
// import just one import { sum } from "example";console.log(sum(1, 2)); // 3sum = 1; // error上述代碼導出了example模塊的sum()函數。不論example模塊export多少個接口,開發者可以根據不同的使用場景import任意個數的接口。上述代碼中嘗試對sum重新賦值,拋出語法錯誤,驗證了被導入的接口變量不能被重新賦值這條規則。
如果想import多個接口變量,可以使用以下方式:
// import multiple import { sum, multiply, magicNumber } from "example"; console.log(sum(1, magicNumber)); // 8 console.log(multiply(1, 2)); // 2上述代碼中導入了example模塊的三個接口變量:sum、multiply和magicNumber。
你還可以將整個模塊導出為一個獨立的對象,其被export的接口變量作為這個對象的屬性使用。如下:
// import everything import * as example from "example"; console.log(example.sum(1,example.magicNumber)); // 8 console.log(example.multiply(1, 2)); // 2上述代碼中,example模塊作為一個整體被導入,以一個名為example的對象使用,example模塊暴露出來的sum()、multiply()和magicNumber作為example對象的屬性使用。
需要注意的是,不論使用import多次導入一個模塊,被導入模塊內部的代碼只會被執行一次。如下:
import { sum } from "example"; import { multiply } from "example"; import { magicNumber } from "example";上述代碼中,使用import導入了3次example模塊,但是example模塊背部的代碼鐘會被執行一次。在第一次被導入后,example模塊被實例化,隨后此實例引用將儲存在內存中。在此之后,不論import多少次,甚至被多個不同的模塊import,都將使用內存中的example模塊實例,而不必重復執行模塊內部的代碼。
接口標識符重命名
通常情況下,為了增強代碼的易讀性,我們往往不直接使用某個變量、函數或者class的原始名稱。ES6的模塊規范允許在導出或導入時修改接口標識符的名稱。
比如,在導出某個函數時希望更改函數名,可以使用as關鍵字進行如下修改:
function sum(num1, num2) {return num1 + num2; }export { sum as add };上述代碼中sum()函數在被導出時將接口函數名更改為add(),其他模塊在導入此接口函數時必須使用add標識符,如下:
import { add } from "example";同理,在導入某個模塊接口函數時,也可以使用as關鍵字修改標識符名稱:
import { add as sum } from "example"; console.log(typeof add); // "undefined" console.log(sum(1, 2)); // 3上述代碼在導入接口函數add()時,將標識符名稱修改為sum。
導入綁定
需要注意import表達式非常重要的一個細節:import的變量、函數或class并不是簡單的引用關系,而是創建了一種綁定關系。換句話說,雖然不能手動修改導入的接口成員,但是可以通過源模塊的邏輯進行修改。比如:
export var name = "Nicholas"; export function setName(newName) {name = newName; }當在其他模塊中導入name和setName()后,可以通過調用setName()修改name的值:
import { name, setName } from "example";console.log(name); // "Nicholas" setName("Greg"); console.log(name); // "Greg"name = "Nicholas"; // error調用setName("Greg")時,實際上回到了setName的源模塊內執行,從而將name的值修改為“Greg”,并且修改后的結果自動映射到了導入name的模塊。
缺省接口
模塊export的缺省接口是由default關鍵字修飾的一個單獨的變量、函數或者class。如下:
export default function(num1, num2) {return num1 + num2; }上述代碼是一個典型的export缺省接口。default關鍵字表明這是一個缺省接口,并且缺省接口的函數不需要指定具體的函數名,因為模塊本身就代表著此接口函數。
也可以將缺省接口重命名,如下:
// equivalent to previous example function sum(num1, num2) {return num1 + num2; }export { sum as default };上述代碼等價于前例,as default表明sum函數作為缺省接口被導出。
每個模塊只能被定義一個缺省接口。嘗試定義多個缺省接口會引起語法錯誤。
導入缺省接口的語法與前文提到的導入整個模塊的語法類似:
// import the default import sum from "example";console.log(sum(1, 2)); // 3上述代碼導入example模塊的缺省接口。請注意導入的缺省接口標識符并沒有包裹在花括號內。這種簡潔的語法形式將成為web應用導入已存對象的常用格式:
import $ from "jquery";如果需要導入某個模塊的缺省接口和非缺省接口,可以在一個表達式中實現。比如某個模塊暴露出以下接口:
export let color = "red";export default function(num1, num2) {return num1 + num2; }可以通過以下形式導入:
import sum, { color } from "example";console.log(sum(1, 2)); // 3 console.log(color); // "red"上述代碼有兩點需要注意:
導入缺省接口時可以重命名標識符:
// equivalent to previous example import { default as sum, color } from "example";console.log(sum(1, 2)); // 3 console.log(color); // "red"上述代碼中,缺省接口標識符default被重命名為sum,連同非缺省接口color一起被包裹在花括號內。
Re-exporting
某些場景下,開發者需要將導入的模塊再次導出,可以使用以下模式:
import { sum } from "example"; export { sum }除此之外,還有一種更簡潔的形式:
export { sum } from "example";上述代碼將example模塊的sum接口再次導出。當然,可以使用as在導出時進行重命名:
export { sum as add } from "example";上述代碼導入example模塊的sum接口,隨后重命名為add再次導出。
使用通配符*可以將模塊作為整體導出:
export * from "example";使用上述代碼導出整個example模塊時,example模塊的缺省接口和非缺省接口全部包括在內,會影響當前模塊的導出行為。比如,如果example模塊有缺省接口,那么就不能在當前模塊中另行定義缺省接口。
非綁定import
某些模塊可能只是對某個全局變量進行了修改,并未導出任何接口。雖然模塊內部的變量、函數和類并不暴露在全局作用域內,但并不意味著模塊內部不能訪問全局域的成員。在某個模塊內對內置對象(比如Array或Object)進行了擴展修改,其他模塊中也會受到影響。
比如,假設現在對Array對象增加一個擴展方法pushAll(),可以在某個模塊內進行以下操作:
// module code without exports or imports Array.prototype.pushAll = function(items) {// items must be an arrayif (!Array.isArray(items)) {throw new TypeError("Argument must be an array.");}// use built-in push() and spread operatorreturn this.push(...items); };雖然上述模塊沒有導出/導入任何接口,但它本身是一個符合規范的模塊。上述代碼可以當作一個模塊使用,也可以作為一段普通的腳本。由于模塊未導出任何接口,你可以使用簡化的import表達式執行模塊代碼,而不必創建綁定關系。如下:
import from "example";let colors = ["red", "green", "blue"]; let items = [];items.pushAll(colors);上述代碼將example模塊導入并執行,Array的擴展方法pushAll()有效,可以在當前模塊的使用。
非綁定的import通常被用來創建polyfill和shim。
譯者注:shim和polyfill是JavaScript應用開發中解決兼容性的方案用語。簡單來說就是使用舊環境的API實現新API。感興趣的讀者可自行查閱相關資料。
總結
ES6引入模塊機制的目標是提供一種代碼功能化的封裝模式。模塊與普通腳本的最大的不同在于其頂層作用域內的變量、函數和class并不會暴露在全局域內,而且this的值為undefined。工作原理的不同,也需要一套全然不同的載入方式支持。
如果想在模塊外部使用本模塊的某些功能,必須使用export關鍵字將其導出。任何變量、函數和class都可以被導出。此外,每個模塊只能導出一個缺省接口。被導出后,其他模塊便可以導入部分或者真個模塊。被導入的接口標識符類似const定義的常量,擁有塊級域綁定特性。
另外,沒有導出任何接口的模塊在被其他模塊導入時不會創建綁定關系。
總結
以上是生活随笔為你收集整理的【译】《Understanding ECMAScript6》- 第八章-Module的全部內容,希望文章能夠幫你解決所遇到的問題。