隨著應用程式規模增長,如何組織和管理程式碼成為關鍵挑戰。本章探討 JavaScript 的模組化技術演進:從 pre-ES6 的手工模式,到 AMD/CommonJS 標準,最終到 ES6 原生模組系統。
11.1 Pre-ES6 的 JavaScript 模組化#
Pre-ES6 的 JavaScript 只有兩種作用域:全域作用域和函式作用域,沒有內建的模組、命名空間或套件機制。開發者必須利用既有的語言特性來實現模組化。
一個模組系統至少需要滿足兩個需求:
- 定義介面(interface):透過它存取模組提供的功能
- 隱藏內部實作(hide module internals):讓使用者不必關心實作細節,同時防止外部對內部狀態的意外修改
使用物件、閉包和立即函式定義模組#
JavaScript 可利用以下語言特性來實現模組的兩大需求:
- 隱藏模組內部:呼叫函式會建立新的作用域,函式內定義的變數只在該函式內可見,因此可用函式來隱藏模組內部變數
- 定義模組介面:從函式模組回傳一個物件,該物件代表模組的公開介面,透過閉包保持對內部變數的存取
模組模式(Module Pattern):
const MouseCounterModule = (function () {
let numClicks = 0; // 私有變數
const handleClick = () => {
// 私有函式
alert(++numClicks);
};
return {
// 公開介面
countClicks: () => {
document.addEventListener("click", handleClick);
},
};
})();這種結合立即函式、物件和閉包來建立模組的模式被稱為 module pattern,由 Douglas Crockford 推廣,是最早流行的 JavaScript 模組化方式之一。
從外部只能存取介面物件上的屬性和方法,而 numClicks 和 handleClick 等內部細節透過閉包保持存活,但無法從外部直接存取。
擴展模組(Augmenting Modules):
可以將現有模組傳入另一個立即函式來擴展功能,而不修改原始模組程式碼:
(function (module) {
let numScrolls = 0;
const handleScroll = () => {
alert(++numScrolls);
};
module.countScrolls = () => {
document.addEventListener("wheel", handleScroll);
};
})(MouseCounterModule);模組擴展有一個根本限制:透過不同的立即函式進行的擴展,彼此無法共享私有模組內部變數,因為每次函式呼叫都會建立新的作用域。
模組模式的其他問題:
- 不處理模組之間的依賴管理
- 開發者必須自行確保正確的依賴載入順序
- 在大型應用中,互相依賴的模組可能造成嚴重問題
使用 AMD 和 CommonJS 模組化#
為解決模組模式的依賴管理問題,出現了兩個競爭標準。
AMD(Asynchronous Module Definition):
- 源自 Dojo toolkit,專為瀏覽器端設計
- 最流行的實作是 RequireJS
- 提供
define函式,接受三個參數:- 模組 ID
- 依賴模組 ID 列表
- 建立模組的工廠函式
define("MouseCounterModule", ["jQuery"], ($) => {
let numClicks = 0;
const handleClick = () => {
alert(++numClicks);
};
return {
countClicks: () => {
$(document).on("click", handleClick);
},
};
});AMD 的優點:
- 自動解析依賴,不需手動管理載入順序
- 模組非同步載入,避免阻塞
- 同一個檔案可定義多個模組
CommonJS:
- 為通用 JavaScript 環境設計,在 Node.js 社群最為流行
- 基於檔案的模組系統,一個檔案一個模組
- 透過
module.exports定義公開介面 - 透過
require同步載入其他模組
// MouseCounterModule.js
const $ = require("jQuery"); // 同步載入依賴
let numClicks = 0;
const handleClick = () => {
alert(++numClicks);
};
module.exports = {
// 定義公開介面
countClicks: () => {
$(document).on("click", handleClick);
},
};// 使用模組
const MouseCounterModule = require("MouseCounterModule.js");
MouseCounterModule.countClicks();CommonJS 的優點:
- 語法簡單,接近標準 JavaScript 寫法
- 是 Node.js 的預設模組格式,可存取 npm 上大量的套件
CommonJS 的缺點:
- 不是為瀏覽器設計,瀏覽器中沒有原生的
module和require支援 - 需要透過 Browserify 或 RequireJS 等工具打包成瀏覽器可用的格式
兩個標準的競爭導致了社群分裂。UMD(Universal Module Definition)嘗試提供同時相容 AMD 和 CommonJS 的語法,但較為繁瑣。最終,ECMAScript 委員會決定制定統一的模組標準。
11.2 ES6 模組#
ES6 模組結合了 AMD 和 CommonJS 的優點:
- 類似 CommonJS,語法簡潔,且基於檔案(一個檔案一個模組)
- 類似 AMD,支援非同步模組載入
核心概念:只有被明確 export 的識別符才能從模組外部存取,其他所有識別符(即使定義在頂層作用域)都只在模組內部可見。
ES6 引入兩個新關鍵字:
export:讓特定識別符可從模組外部存取import:匯入其他模組導出的識別符
導出與匯入功能#
命名導出(Named Export):
// Ninja.js
export const message = "Hello";
export function sayHiToNinja() {
return message + " " + ninja;
}也可以在模組末尾統一導出:
export { message, sayHiToNinja };匯入命名導出:
import { message, sayHiToNinja } from "Ninja.js";未導出的變數(如模組內的 ninja)無法從外部存取。
匯入所有命名導出:
import * as ninjaModule from "Ninja.js";
ninjaModule.message; // "Hello"
ninjaModule.sayHiToNinja(); // "Hello Yoshi"
預設導出(Default Export):
當模組主要代表一個單一實體(如一個 class)時,使用預設導出:
export default class Ninja {
constructor(name) {
this.name = name;
}
}
export function compareNinjas(ninja1, ninja2) {
return ninja1.name === ninja2.name;
}匯入預設導出時不需要大括號,且可以使用任意名稱:
import ImportedNinja from "Ninja.js";
import { compareNinjas } from "Ninja.js";
// 或簡寫為
import ImportedNinja, { compareNinjas } from "Ninja.js";重新命名導出和匯入:
使用 as 關鍵字可以重新命名:
// 導出端
export { sayHi as sayHello };
// 匯入端
import { greet as sayHello } from "Hello.js";
import { greet as salute } from "Salute.js";這在避免命名衝突時特別有用。
ES6 模組語法總覽:
| 語法 | 意義 |
|---|---|
export const ninja = "Yoshi"; | 導出命名變數 |
export function compare(){} | 導出命名函式 |
export class Ninja{} | 導出命名類別 |
export default class Ninja{} | 導出預設類別 |
export { ninja, compare }; | 導出既有變數 |
export { ninja as samurai }; | 以新名稱導出 |
import Ninja from "Ninja.js"; | 匯入預設導出 |
import { ninja, Ninja } from "Ninja.js"; | 匯入命名導出 |
import * as Ninja from "Ninja.js"; | 匯入所有命名導出 |
import { ninja as iNinja } from "Ninja.js"; | 以新名稱匯入 |
11.3 本章重點#
- 大型單體程式碼庫難以理解和維護,應將程式拆分為較小的、鬆耦合的模組
- 模組是比物件和函式更大的程式碼組織單元,促進可理解性、可維護性和可重用性
- Pre-ES6 沒有內建模組系統,開發者利用立即函式建立作用域、閉包保持變數存活、物件定義介面,形成 module pattern
- AMD 為瀏覽器設計,支援非同步載入和自動依賴解析
- CommonJS 語法簡潔,同步載入,更適合伺服器端,有大量 npm 套件可用
- ES6 模組結合了兩者優點:以檔案為基礎、語法受 CommonJS 啟發、支援非同步載入如 AMD
- 使用
export關鍵字導出識別符 - 使用
import關鍵字匯入識別符 - 模組可有一個
default導出 - 導出和匯入都可使用
as關鍵字重新命名
- 使用