模塊
編寫易于刪除但不易擴展的代碼。
以下是一個用Python展示如何編寫相對容易刪除但難以擴展的代碼示例:
# 此函數(shù)專門用于一次性的數(shù)據(jù)計算
def calculate_specific_data():
data = [1, 2, 3, 4, 5]
result = 0
for num in data:
if num > 2:
result += num * 2
return result
specific_result = calculate_specific_data()
print(specific_result)
解釋:
-
易于刪除:
calculate_specific_data函數(shù)是自包含的。如果不再需要這個特定的計算,你只需刪除函數(shù)定義以及調(diào)用它的代碼行即可。它不依賴復(fù)雜的外部模塊或全局狀態(tài),所以刪除起來并不困難。 -
難以擴展:
- 數(shù)據(jù)列表
[1, 2, 3, 4, 5]是在函數(shù)內(nèi)部硬編碼的。如果你想使用不同的數(shù)據(jù),就需要直接修改函數(shù)體。 - 計算邏輯(
if num > 2: result += num * 2)非常特定。添加新的條件或操作需要對函數(shù)內(nèi)現(xiàn)有的循環(huán)結(jié)構(gòu)進行重大修改。這里沒有像函數(shù)參數(shù)或回調(diào)這樣明確的可擴展點,若不觸及核心實現(xiàn),就無法修改其行為。
- 數(shù)據(jù)列表
在更面向?qū)ο蠡蚰K化的編程風(fēng)格中,我們通常會為可擴展性進行設(shè)計,但這個示例卻違背了這一原則。
以下是等效的JavaScript代碼:
function calculateSpecificData() {
let data = [1, 2, 3, 4, 5];
let result = 0;
for (let num of data) {
if (num > 2) {
result += num * 2;
}
}
return result;
}
let specificResult = calculateSpecificData();
console.log(specificResult);
在這段JavaScript代碼中,與Python示例一樣,存在易于刪除和難以擴展的特點。數(shù)據(jù)和計算邏輯在函數(shù)內(nèi)部緊密耦合,這使得優(yōu)雅地擴展功能變得困難。
理想情況下,一個程序具有清晰、直接的結(jié)構(gòu)。其運行方式易于解釋,并且每個部分都發(fā)揮著明確界定的作用。
在實際中,程序是自然生長的。隨著程序員發(fā)現(xiàn)新的需求,功能片段會不斷添加。要保持這樣一個程序結(jié)構(gòu)良好,需要持續(xù)的關(guān)注和投入。而這種投入只有在未來,下次有人對該程序進行開發(fā)時才會得到回報,所以很容易讓人忽視它,任由程序的各個部分變得錯綜復(fù)雜。
這會引發(fā)兩個實際問題。首先,理解一個錯綜復(fù)雜的系統(tǒng)很困難。如果所有部分都能相互影響,那么就很難孤立地審視任何一個特定部分。你不得不對整個系統(tǒng)形成一個全面的理解。其次,如果你想在其他情境中使用這樣一個程序的任何功能,重寫它可能比試圖將其從原有的上下文環(huán)境中剝離出來更容易。
“一團亂麻”這個說法常被用來形容這類龐大且無結(jié)構(gòu)的程序。所有東西都粘連在一起,當(dāng)你試圖挑出一塊時,整個東西就散架了,而你最終只會弄得一團糟。
模塊化程序
模塊就是為避免這些問題而產(chǎn)生的。一個模塊是程序的一個組成部分,它會指明自己依賴哪些其他部分,以及為其他模塊提供哪些可使用的功能(即它的接口)。
正如我們在第6章所了解到的,模塊接口與對象接口有很多共同之處。它們將模塊的一部分功能對外開放,而將其余部分設(shè)為私有。
然而,模塊為其他模塊提供的可用接口只是其中一方面。一個優(yōu)秀的模塊系統(tǒng)還要求模塊指明它們使用了其他模塊的哪些代碼。這些關(guān)系被稱為依賴關(guān)系。如果模塊A使用了模塊B的功能,那么就說模塊A依賴于模塊B。當(dāng)這些依賴關(guān)系在模塊自身中被清晰地指定后,就可以據(jù)此確定要使用某個特定模塊還需要哪些其他模塊,并能自動加載其依賴項。
當(dāng)模塊之間的交互方式明確時,一個系統(tǒng)就更像樂高積木,各部件通過定義明確的連接件相互作用,而不再像一團亂麻,所有東西都混在一起。
ES 模塊
最初的 JavaScript 語言并沒有模塊的概念。所有腳本都在相同的作用域中運行,要訪問另一個腳本中定義的函數(shù),需通過引用該腳本創(chuàng)建的全局綁定來實現(xiàn)。這實際上助長了代碼間不經(jīng)意且難以察覺的糾纏,并引發(fā)了諸如不相關(guān)腳本試圖使用相同綁定名稱之類的問題。
自 ECMAScript 2015 起,JavaScript 支持兩種不同類型的程序。腳本的行為方式依舊沿用舊有模式:它們的綁定在全局作用域中定義,且無法直接引用其他腳本。而模塊擁有自己獨立的作用域,并支持 import 和 export 關(guān)鍵字(腳本中無法使用這兩個關(guān)鍵字),用于聲明其依賴關(guān)系和接口。這種模塊系統(tǒng)通常被稱為 ES 模塊(其中 ES 代表 ECMAScript)。
一個模塊化程序由許多這樣的模塊組成,這些模塊通過導(dǎo)入(import)和導(dǎo)出(export)相互連接。
以下示例模塊用于在日期名稱和數(shù)字(如 Date 的 getDay 方法返回的數(shù)字)之間進行轉(zhuǎn)換。它定義了一個不屬于其接口的常量,以及兩個屬于接口的函數(shù)。該模塊沒有依賴項。
// 定義一個包含星期名稱的數(shù)組
const names = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
// 導(dǎo)出一個函數(shù),該函數(shù)根據(jù)傳入的數(shù)字返回對應(yīng)的星期名稱
export function dayName(number) {
return names[number];
}
// 導(dǎo)出一個函數(shù),該函數(shù)根據(jù)傳入的星期名稱返回對應(yīng)的數(shù)字
export function dayNumber(name) {
return names.indexOf(name);
}
上述代碼定義了一個模塊,其中包含一個私有常量 names(因為沒有通過 export 暴露出去),以及兩個導(dǎo)出的公共函數(shù) dayName 和 dayNumber 。dayName 函數(shù)接受一個數(shù)字參數(shù),返回對應(yīng)的星期名稱;dayNumber 函數(shù)接受一個星期名稱參數(shù),返回對應(yīng)的數(shù)字索引。
export關(guān)鍵字可以置于函數(shù)、類或綁定定義之前,用以表明該綁定是模塊接口的一部分。這使得其他模塊能夠通過導(dǎo)入操作來使用該綁定。
import {dayName} from "./dayname.js";
let now = new Date();
console.log(`Today is ${dayName(now.getDay())}`);
// → Today is Monday
這段代碼的作用是從一個名為dayname.js的模塊中導(dǎo)入dayName函數(shù),然后使用這個函數(shù)來獲取當(dāng)前日期是星期幾,并將結(jié)果打印到控制臺。
以下是詳細解釋:
-
import {dayName} from "./dayname.js";- 這行代碼使用ES模塊的
import語句,從當(dāng)前目錄下的dayname.js文件中導(dǎo)入dayName函數(shù)。{}用于指定要導(dǎo)入的具體綁定(這里是dayName函數(shù))。
- 這行代碼使用ES模塊的
-
let now = new Date();- 創(chuàng)建一個
Date對象now,它代表當(dāng)前的日期和時間。
- 創(chuàng)建一個
-
console.log(Today is ${dayName(now.getDay())});-
now.getDay()獲取當(dāng)前日期是一周中的第幾天(0代表星期日,1代表星期一,以此類推)。 - 然后
dayName函數(shù)接收這個數(shù)字,并返回對應(yīng)的星期名稱。 - 最后,使用模板字面量將結(jié)果打印到控制臺,例如,如果今天是星期一,控制臺將輸出
Today is Monday。
-
整體來說,這段代碼展示了如何在一個模塊中導(dǎo)入另一個模塊的函數(shù),并利用它進行日期相關(guān)的操作。
import關(guān)鍵字后面跟著花括號內(nèi)的綁定名稱列表,可使來自另一個模塊的綁定在當(dāng)前模塊中可用。模塊通過帶引號的字符串來標識。
不同平臺將模塊名解析為實際程序的方式有所不同。瀏覽器將它們視為網(wǎng)址,而Node.js則將它們解析為文件。當(dāng)你運行一個模塊時,它所依賴的所有其他模塊,以及這些其他模塊所依賴的模塊都會被加載,并且導(dǎo)出的綁定可供導(dǎo)入它們的模塊使用。
import和export聲明不能出現(xiàn)在函數(shù)、循環(huán)或其他代碼塊內(nèi)部。無論模塊中的代碼如何執(zhí)行,在模塊加載時,它們都會立即被解析。為體現(xiàn)這一點,它們必須僅出現(xiàn)在模塊的外部主體中。
因此,一個模塊的接口由一組命名綁定組成,依賴該模塊的其他模塊可以訪問這些綁定。導(dǎo)入的綁定可以使用as關(guān)鍵字在其名稱之后進行重命名,從而賦予它們一個新的本地名稱。
import {dayName as nomDeJour} from "./dayname.js";
console.log(nomDeJour(3));
// → Wednesday
在這段代碼中,import {dayName as nomDeJour} from "./dayname.js"; 語句從 "./dayname.js" 模塊導(dǎo)入 dayName 函數(shù),并將其重命名為 nomDeJour。這意味著在當(dāng)前模塊中,nomDeJour 就代表了從 dayname.js 模塊導(dǎo)入的 dayName 函數(shù)。
接著,console.log(nomDeJour(3)); 調(diào)用重命名后的函數(shù) nomDeJour,并傳入?yún)?shù) 3。由于 dayName 函數(shù)(在這里以 nomDeJour 的名字被調(diào)用)會根據(jù)傳入的數(shù)字返回對應(yīng)的星期名稱,數(shù)字 3 對應(yīng)的是星期三,所以最終控制臺會輸出 "Wednesday"。
這種導(dǎo)入時重命名的方式在多個模塊可能存在命名沖突,或者希望使用更符合本地語義的名稱時非常有用。
一個模塊還可以有一個名為 default 的特殊導(dǎo)出,這通常用于只導(dǎo)出單個綁定的模塊。要定義默認導(dǎo)出,你可以在表達式、函數(shù)聲明或類聲明之前寫上 export default 。
export default ["Winter", "Spring", "Summer", "Autumn"];
這樣的綁定在導(dǎo)入時,導(dǎo)入名稱周圍無需使用花括號。
import seasonNames from "./seasonname.js";
要一次性導(dǎo)入一個模塊的所有綁定,可以使用 import * 。你提供一個名稱,該名稱將綁定到一個包含該模塊所有導(dǎo)出內(nèi)容的對象上。當(dāng)你要使用許多不同的導(dǎo)出時,這種方式會很有用。
import * as dayName from "./dayname.js";
console.log(dayName.dayName(3));
// → Wednesday
軟件包
將程序構(gòu)建成一個個獨立的部分,并能讓其中一些部分獨立運行,這樣做的好處之一是,你或許能在不同程序中復(fù)用同一個部分。
但要如何實現(xiàn)呢?比如說,我想在另一個程序中使用第9章中的parseINI函數(shù)。如果明確知道該函數(shù)依賴什么(在這個例子中,不依賴任何東西),我可以直接把那個模塊復(fù)制到我的新項目中使用。但這樣一來,如果我發(fā)現(xiàn)代碼中有個錯誤,我很可能只在當(dāng)時正在處理的那個程序中修復(fù)它,而忘記在另一個程序中也進行修復(fù)。
一旦開始復(fù)制代碼,你很快就會發(fā)現(xiàn)自己在浪費時間和精力來四處轉(zhuǎn)移代碼副本,并讓它們保持更新。這就是軟件包發(fā)揮作用的地方。軟件包是一段可分發(fā)(復(fù)制和安裝)的代碼。它可能包含一個或多個模塊,并且包含關(guān)于它依賴哪些其他軟件包的信息。軟件包通常還會附帶文檔,解釋其功能,這樣即便不是編寫者本人,其他人也有可能使用它。
當(dāng)在軟件包中發(fā)現(xiàn)問題或添加新功能時,軟件包就會更新?,F(xiàn)在依賴它的程序(這些程序本身也可能是軟件包)可以復(fù)制新版本,從而獲得代碼改進帶來的好處。
以這種方式工作需要相應(yīng)的基礎(chǔ)設(shè)施。我們需要一個存儲和查找軟件包的地方,以及一種方便的安裝和升級方式。在JavaScript領(lǐng)域,這個基礎(chǔ)設(shè)施由NPM(https://npmjs.com)提供。
NPM有兩個含義:一是一個在線服務(wù),你可以在上面下載(和上傳)軟件包;二是一個程序(與Node.js捆綁在一起),幫助你安裝和管理軟件包。
在撰寫本文時,NPM上有超過三百萬個不同的軟件包。說實話,其中很大一部分沒什么用。但幾乎所有有用的、公開可用的JavaScript軟件包都能在NPM上找到。例如,一個類似于我們在第9章構(gòu)建的INI文件解析器,可在名為ini的軟件包中找到。
第20章將展示如何使用npm命令行程序在本地安裝此類軟件包。
有高質(zhì)量的軟件包可供下載非常有價值。這意味著我們常??梢员苊庵貜?fù)編寫已經(jīng)有一百個人寫過的程序,只需按幾個鍵就能獲得一個可靠且經(jīng)過良好測試的實現(xiàn)。
軟件復(fù)制成本很低,所以一旦有人編寫完成,將其分發(fā)給其他人是一個高效的過程。不過,首先編寫軟件是一項工作,而回應(yīng)那些發(fā)現(xiàn)代碼問題或想要提出新功能的人則更是一項工作。
默認情況下,你擁有自己編寫的代碼的版權(quán),其他人只有在獲得你許可的情況下才能使用。但由于有些人很友善,而且發(fā)布優(yōu)秀的軟件可以讓你在程序員群體中稍微出名一點,許多軟件包都是在明確允許他人使用的許可協(xié)議下發(fā)布的。
NPM上的大多數(shù)代碼都是以這種方式授權(quán)的。有些許可協(xié)議要求你在同一許可下發(fā)布基于該軟件包構(gòu)建的代碼。其他許可協(xié)議要求則沒那么嚴格,只要求你在分發(fā)代碼時保留許可協(xié)議。JavaScript社區(qū)大多使用后一種類型的許可協(xié)議。在使用他人的軟件包時,要確保你了解它們的許可協(xié)議。
現(xiàn)在,我們不用自己編寫INI文件解析器了,可以使用NPM上的解析器。
import {parse} from "ini";
console.log(parse("x = 10\ny = 20"));
// → {x: "10", y: "20"}
這段代碼使用了來自 ini 包的 parse 函數(shù)來解析INI格式的字符串。
-
import {parse} from "ini";- 這里使用ES模塊的
import語句,從名為ini的包中導(dǎo)入parse函數(shù)。在JavaScript中,當(dāng)從包中導(dǎo)入模塊時,不需要像導(dǎo)入本地模塊那樣指定文件路徑。ini包是一個在NPM上可用的包,提供了處理INI文件格式的功能。
- 這里使用ES模塊的
-
console.log(parse("x = 10\ny = 20"));- 調(diào)用導(dǎo)入的
parse函數(shù),并傳入一個包含INI格式數(shù)據(jù)的字符串"x = 10\ny = 20"。parse函數(shù)會解析這個字符串,并將其轉(zhuǎn)換為JavaScript對象。 - 最后,使用
console.log將解析后的對象打印到控制臺。在這個例子中,輸出為{x: "10", y: "20"},展示了parse函數(shù)成功將INI格式字符串解析為JavaScript對象,其中鍵x和y分別對應(yīng)其在INI字符串中的值。
- 調(diào)用導(dǎo)入的
這段代碼展示了如何利用NPM包中提供的功能,簡化對特定格式數(shù)據(jù)的處理,避免了自行編寫復(fù)雜的解析邏輯。
CommonJS模塊
在2015年之前,JavaScript語言還沒有內(nèi)置的模塊系統(tǒng),但人們已經(jīng)開始用JavaScript構(gòu)建大型系統(tǒng)。為了讓這切實可行,他們需要模塊。
社區(qū)在JavaScript語言基礎(chǔ)上自行設(shè)計了臨時的模塊系統(tǒng)。這些系統(tǒng)利用函數(shù)為模塊創(chuàng)建局部作用域,并使用普通對象來表示模塊接口。
最初,人們只是手動將整個模塊包裹在一個“立即調(diào)用函數(shù)表達式”中,以此創(chuàng)建模塊作用域,并將他們的接口對象賦值給一個全局變量。
const weekDay = function() {
const names = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
return {
name(number) { return names[number]; },
number(name) { return names.indexOf(name); }
};
}();
console.log(weekDay.name(weekDay.number("Sunday")));
// → Sunday
這段代碼定義了一個模擬模塊功能的結(jié)構(gòu),雖然在ES6模塊之前沒有原生模塊系統(tǒng),但通過這種方式實現(xiàn)了類似模塊的封裝效果。以下是詳細解釋:
-
定義模塊函數(shù)并立即調(diào)用:
const weekDay = function() { ... }();- 這里定義了一個匿名函數(shù),然后通過末尾的
()立即調(diào)用它,并將返回值賦值給weekDay變量。這個匿名函數(shù)內(nèi)部的代碼就像是模塊內(nèi)部的代碼,具有自己的局部作用域。
-
模塊內(nèi)部的實現(xiàn):
const names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];- 在函數(shù)內(nèi)部定義了一個
names數(shù)組,這個數(shù)組在函數(shù)外部是不可直接訪問的,類似于模塊內(nèi)部的私有數(shù)據(jù)。 return { name(number) { return names[number]; }, number(name) { return names.indexOf(name); } };- 函數(shù)返回一個對象,這個對象包含兩個方法
name和number。這兩個方法就構(gòu)成了類似模塊的接口,外部代碼可以通過weekDay變量訪問到這兩個方法,而names數(shù)組對外部是隱藏的。
-
使用模塊功能:
console.log(weekDay.name(weekDay.number("Sunday")));- 首先調(diào)用
weekDay.number("Sunday"),這個方法會返回Sunday在names數(shù)組中的索引(這里是0)。 - 然后將這個索引作為參數(shù)傳遞給
weekDay.name方法,weekDay.name(0)會返回names數(shù)組中索引為0的元素,即"Sunday"。最后通過console.log將結(jié)果打印出來。
總的來說,這段代碼通過立即調(diào)用函數(shù)表達式模擬了模塊的封裝,將數(shù)據(jù)和功能封裝在一個作用域內(nèi),并通過返回的對象提供了外部可訪問的接口。
這種模塊風(fēng)格在一定程度上提供了隔離性,但它并未聲明依賴關(guān)系。相反,它只是將其接口放入全局作用域,并期望其依賴項(如果有的話)也這樣做。這并不理想。
如果我們實現(xiàn)自己的模塊加載器,就能做得更好。在JavaScript中,最廣泛使用的附加模塊方法被稱為CommonJS模塊。Node.js從一開始就使用這種模塊系統(tǒng)(盡管它現(xiàn)在也知道如何加載ES模塊),并且它是NPM上許多軟件包所使用的模塊系統(tǒng)。
一個CommonJS模塊看起來就像一個普通腳本,但它可以訪問兩個用于與其他模塊交互的綁定。第一個是一個名為require的函數(shù)。當(dāng)你使用依賴模塊的名稱調(diào)用這個函數(shù)時,它會確保該模塊已加載,并返回其接口。第二個是一個名為exports的對象,它是該模塊的接口對象。它初始為空,你可以向其添加屬性來定義導(dǎo)出的值。
這個CommonJS示例模塊提供了一個日期格式化函數(shù)。它使用了NPM上的兩個軟件包——ordinal用于將數(shù)字轉(zhuǎn)換為諸如“1st”和“2nd”這樣的字符串,以及date - names用于獲取工作日和月份的英文名稱。它導(dǎo)出一個名為formatDate的函數(shù),該函數(shù)接受一個Date對象和一個模板字符串。
模板字符串可能包含用于指示格式的代碼,例如YYYY表示完整年份,Do表示月份中的序數(shù)日期。你可以給它一個像“MMMM Do YYYY”這樣的字符串,以獲得像“November 22nd 2017”這樣的輸出。
const ordinal = require("ordinal");
const {days, months} = require("date-names");
exports.formatDate = function(date, format) {
return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => {
if (tag == "YYYY") return date.getFullYear();
if (tag == "M") return date.getMonth();
if (tag == "MMMM") return months[date.getMonth()];
if (tag == "D") return date.getDate();
if (tag == "Do") return ordinal(date.getDate());
if (tag == "dddd") return days[date.getDay()];
});
};
這段代碼是一個使用CommonJS模塊規(guī)范的JavaScript模塊,用于格式化日期。以下是對代碼的詳細解釋:
-
引入依賴模塊:
-
const ordinal = require("ordinal");:使用require函數(shù)引入名為ordinal的NPM包,該包用于將數(shù)字轉(zhuǎn)換為序數(shù)形式,如“1st”“2nd”等,并將其賦值給ordinal變量。 -
const {days, months} = require("date - names");:從date - names包中解構(gòu)出days(用于獲取星期幾的名稱)和months(用于獲取月份的名稱),并分別賦值給同名變量。
-
-
定義導(dǎo)出函數(shù):
-
exports.formatDate = function(date, format) {... };:在exports對象上定義一個名為formatDate的函數(shù),這是該模塊對外暴露的接口。這個函數(shù)接受兩個參數(shù):date(一個Date對象)和format(一個用于指定日期格式的模板字符串)。
-
-
日期格式化邏輯:
-
return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => {... });:使用replace方法對format模板字符串進行替換操作。replace的第一個參數(shù)是一個正則表達式,用于匹配各種日期格式標簽。第二個參數(shù)是一個回調(diào)函數(shù),當(dāng)正則表達式匹配到內(nèi)容時,會將匹配到的標簽(tag)傳入回調(diào)函數(shù)進行處理。
-
-
具體標簽替換邏輯:
-
if (tag == "YYYY") return date.getFullYear();:如果匹配到YYYY標簽,返回date對象的完整年份。 -
if (tag == "M") return date.getMonth();:如果匹配到M標簽,返回date對象的月份(從0開始)。 -
if (tag == "MMMM") return months[date.getMonth()];:如果匹配到MMMM標簽,返回對應(yīng)月份的英文全稱,通過months數(shù)組和date對象的月份索引獲取。 -
if (tag == "D") return date.getDate();:如果匹配到D標簽,返回date對象的日期。 -
if (tag == "Do") return ordinal(date.getDate());:如果匹配到Do標簽,使用ordinal函數(shù)將date對象的日期轉(zhuǎn)換為序數(shù)形式并返回。 -
if (tag == "dddd") return days[date.getDay()];:如果匹配到dddd標簽,返回對應(yīng)星期幾的英文全稱,通過days數(shù)組和date對象的星期索引獲取。
-
總的來說,這個模塊通過引入外部包并定義一個格式化函數(shù),實現(xiàn)了根據(jù)給定的模板字符串格式化Date對象的功能。
ordinal 的接口是一個單一函數(shù),而 date - names 導(dǎo)出的是一個包含多個內(nèi)容的對象 —— days 和 months 是名稱數(shù)組。在為導(dǎo)入的接口創(chuàng)建綁定關(guān)系時,解構(gòu)非常方便。
該模塊將其接口函數(shù)添加到 exports 中,以便依賴它的模塊能夠訪問該函數(shù)。我們可以像這樣使用該模塊:
const {formatDate} = require("./format-date.js");
console.log(formatDate(new Date(2017, 9, 13),
"dddd the Do"));
// → Friday the 13th
這段代碼展示了如何使用前面定義的 format - date.js 模塊中的 formatDate 函數(shù)來格式化日期。
-
導(dǎo)入模塊:
-
const {formatDate} = require("./format - date.js");使用require函數(shù)從當(dāng)前目錄下的format - date.js文件中導(dǎo)入formatDate函數(shù)。這里使用了解構(gòu)賦值,直接將導(dǎo)入的formatDate函數(shù)賦值給同名變量formatDate。
-
-
調(diào)用格式化函數(shù)并輸出結(jié)果:
console.log(formatDate(new Date(2017, 9, 13), "dddd the Do"));-
new Date(2017, 9, 13)創(chuàng)建一個Date對象,表示2017年10月13日(月份從0開始計數(shù),所以9代表10月)。 - 然后將這個
Date對象和模板字符串"dddd the Do"作為參數(shù)傳遞給formatDate函數(shù)。formatDate函數(shù)會根據(jù)模板字符串中的標簽對日期進行格式化。 - 最后,使用
console.log將格式化后的日期字符串輸出到控制臺。在這個例子中,輸出為"Friday the 13th",符合模板字符串的格式要求。
這段代碼演示了如何在CommonJS模塊系統(tǒng)中導(dǎo)入和使用自定義模塊來實現(xiàn)日期格式化功能。
CommonJS是通過一個模塊加載器來實現(xiàn)的。當(dāng)加載一個模塊時,模塊加載器會將模塊的代碼包裝在一個函數(shù)中(賦予其自身的局部作用域),并將require和exports綁定作為參數(shù)傳遞給該函數(shù)。
如果我們假設(shè)可以使用readFile函數(shù),該函數(shù)通過文件名讀取文件并返回文件內(nèi)容,那么我們可以像這樣定義一個簡化版的require函數(shù):
function require(name) {
if (!(name in require.cache)) {
let code = readFile(name);
let exports = require.cache[name] = {};
let wrapper = Function("require, exports", code);
wrapper(require, exports);
}
return require.cache[name];
}
require.cache = Object.create(null);
這段代碼定義了一個簡化的 require 函數(shù),模擬了CommonJS模塊加載機制。以下是對代碼的詳細解釋:
-
檢查緩存:
-
if (!(name in require.cache)) {... }:require.cache是一個對象,用于存儲已經(jīng)加載過的模塊。這行代碼檢查名為name的模塊是否已經(jīng)在緩存中。如果不在緩存中,則執(zhí)行后續(xù)的加載操作。
-
-
讀取模塊代碼:
-
let code = readFile(name);:假設(shè)存在readFile函數(shù),它根據(jù)模塊名name讀取相應(yīng)文件的內(nèi)容,并將內(nèi)容賦值給code變量。這里的readFile函數(shù)是自定義的,在實際實現(xiàn)中應(yīng)提供從文件系統(tǒng)讀取文件的邏輯。
-
-
初始化緩存和導(dǎo)出對象:
-
let exports = require.cache[name] = {};:在require.cache中為當(dāng)前模塊創(chuàng)建一個緩存條目,并初始化一個空的exports對象。這個exports對象將用于存儲模塊導(dǎo)出的內(nèi)容。
-
-
創(chuàng)建包裝函數(shù):
-
let wrapper = Function("require, exports", code);:使用Function構(gòu)造函數(shù)創(chuàng)建一個新的函數(shù)wrapper。這個函數(shù)接受require和exports作為參數(shù),函數(shù)體是從文件中讀取的模塊代碼code。這相當(dāng)于將模塊代碼包裝在一個函數(shù)中,為模塊提供了自己的局部作用域。
-
-
執(zhí)行包裝函數(shù):
-
wrapper(require, exports);:執(zhí)行wrapper函數(shù),并傳入require和exports對象。模塊代碼在執(zhí)行過程中,可以通過require引入其他模塊,通過向exports對象添加屬性來導(dǎo)出內(nèi)容。
-
-
返回緩存中的模塊:
-
return require.cache[name];:無論模塊是否已經(jīng)在緩存中,最后都返回require.cache中對應(yīng)模塊的導(dǎo)出內(nèi)容。
-
-
初始化緩存對象:
-
require.cache = Object.create(null);:初始化require.cache為一個空對象,用于存儲加載過的模塊。Object.create(null)創(chuàng)建的對象沒有原型鏈,相比于普通的空對象{},在某些場景下可以避免意外的屬性訪問問題。
-
這個簡化的 require 函數(shù)展示了CommonJS模塊加載器的基本工作原理,通過緩存機制避免重復(fù)加載模塊,通過包裝函數(shù)為模塊提供獨立的作用域,并管理模塊之間的依賴關(guān)系。
函數(shù)構(gòu)造器 Function
Function 是JavaScript的內(nèi)置函數(shù),它接受一系列參數(shù)(以逗號分隔的字符串形式)以及包含函數(shù)體的字符串,并返回一個具有這些參數(shù)和函數(shù)體的函數(shù)值。這是一個有趣的概念,它允許程序從字符串?dāng)?shù)據(jù)創(chuàng)建新的程序片段,但同時也是危險的。因為如果有人能誘使你的程序?qū)⑺麄兲峁┑淖址湃?Function 中,他們就能讓程序做任何他們想做的事。
標準JavaScript并沒有提供像 readFile 這樣的函數(shù),但不同的JavaScript環(huán)境,比如瀏覽器和Node.js,都有各自訪問文件的方式。這里只是假設(shè) readFile 存在。
require 與模塊緩存
為了避免多次加載同一個模塊,require 維護了一個已加載模塊的存儲(緩存)。每次調(diào)用時,它首先檢查請求的模塊是否已被加載,如果沒有,則進行加載。這包括讀取模塊代碼、將其包裝在一個函數(shù)中并調(diào)用該函數(shù)。
通過將 require 和 exports 定義為生成的包裝函數(shù)的參數(shù)(并在調(diào)用時傳遞適當(dāng)?shù)闹担?,加載器確保這些綁定在模塊作用域中可用。
與ES模塊的區(qū)別
這個系統(tǒng)與ES模塊之間一個重要的區(qū)別在于,ES模塊的導(dǎo)入在模塊腳本開始運行之前就發(fā)生了,而 require 是一個普通函數(shù),在模塊已經(jīng)運行時被調(diào)用。與 import 聲明不同,require 調(diào)用可以出現(xiàn)在函數(shù)內(nèi)部,并且依賴項的名稱可以是任何能求值為字符串的表達式,而 import 只允許普通的帶引號字符串。
JavaScript社區(qū)從CommonJS風(fēng)格過渡到ES模塊的過程緩慢且有些艱難。幸運的是,現(xiàn)在NPM上大多數(shù)流行的軟件包都將其代碼作為ES模塊提供,并且Node.js允許ES模塊從CommonJS模塊導(dǎo)入。雖然你仍可能遇到CommonJS代碼,但實際上已經(jīng)沒有理由再以這種風(fēng)格編寫新程序了。
構(gòu)建與打包
從技術(shù)上講,許多JavaScript軟件包并非用JavaScript編寫。像TypeScript這樣的語言擴展(第8章提到的類型檢查方言)被廣泛使用。人們還常常在實際運行JavaScript的平臺添加某些新語言特性之前很久,就開始使用這些特性。為了實現(xiàn)這一點,他們會編譯代碼,將所選的JavaScript方言轉(zhuǎn)換為普通的舊版JavaScript,甚至轉(zhuǎn)換為JavaScript的早期版本,以便瀏覽器能夠運行。
在網(wǎng)頁中包含一個由200個不同文件組成的模塊化程序會帶來一些問題。如果通過網(wǎng)絡(luò)獲取單個文件需要50毫秒,加載整個程序則需要10秒,如果你能同時加載幾個文件,可能會縮短一半時間。這浪費了大量時間。由于獲取單個大文件往往比獲取許多小文件更快,網(wǎng)頁程序員開始使用工具,在將程序發(fā)布到網(wǎng)頁之前,將他們精心拆分成模塊的程序合并為一個大文件。這樣的工具被稱為打包器。
我們還可以更進一步。除了文件數(shù)量,文件大小也決定了它們通過網(wǎng)絡(luò)傳輸?shù)乃俣?。因此,JavaScript社區(qū)發(fā)明了壓縮器。這些工具獲取一個JavaScript程序,通過自動刪除注釋和空白、重命名綁定以及用占用空間更少的等效代碼替換部分代碼,使程序變小。
在NPM軟件包中或在網(wǎng)頁上運行的代碼,經(jīng)過多個轉(zhuǎn)換階段并不少見——從現(xiàn)代JavaScript轉(zhuǎn)換為舊版JavaScript,將模塊合并為單個文件,然后壓縮代碼。本書不會深入介紹這些工具的細節(jié),因為它們有很多,而且流行的工具經(jīng)常變化。只需知道有這些工具存在,需要時查閱相關(guān)資料即可。
模塊設(shè)計
程序結(jié)構(gòu)設(shè)計是編程中較為微妙的一個方面。任何非簡單的功能模塊都可以通過多種方式進行組織。
良好的程序設(shè)計具有主觀性——其中涉及權(quán)衡取舍,也關(guān)乎個人偏好。學(xué)習(xí)良好結(jié)構(gòu)設(shè)計價值的最佳方式,就是閱讀大量程序或參與眾多編程項目,留意哪些設(shè)計有效,哪些無效。不要認為一團糟的代碼 “只能這樣”。只要多花些心思,幾乎所有代碼的結(jié)構(gòu)都能得到改善。
模塊設(shè)計的一個重要方面是易用性。如果你設(shè)計的內(nèi)容打算供多人使用,或者即便只是供自己在三個月后使用,那時你可能已經(jīng)不記得自己當(dāng)初具體做了什么,那么一個簡單且可預(yù)測的接口會很有幫助。
這可能意味著遵循現(xiàn)有的慣例。ini 包就是一個很好的例子。這個模塊模仿標準的JSON對象,提供了 parse 和 stringify(用于編寫INI文件)函數(shù),并且和JSON一樣,在字符串和普通對象之間進行轉(zhuǎn)換。它的接口簡潔且為人熟知,一旦你使用過一次,就很可能記住如何再次使用它。
即使沒有可模仿的標準函數(shù)或廣泛使用的包,你也可以通過使用簡單的數(shù)據(jù)結(jié)構(gòu)并專注于單一功能,來使你的模塊具有可預(yù)測性。例如,NPM上的許多INI文件解析模塊都提供了一個函數(shù),該函數(shù)直接從硬盤讀取這樣的文件并進行解析。這使得在瀏覽器中無法使用這些模塊,因為在瀏覽器中我們沒有直接訪問文件系統(tǒng)的權(quán)限,而且這種做法增加了復(fù)雜性,若將該模塊與一些文件讀取函數(shù)組合使用,原本可以更好地解決這個問題。
這就引出了模塊設(shè)計的另一個有益方面——與其他代碼的可組合性。專注于計算值的模塊,相比那些執(zhí)行復(fù)雜操作且有副作用的大型模塊,能在更廣泛的程序中適用。一個堅持從磁盤讀取文件的INI文件讀取器,在文件內(nèi)容來自其他來源的場景中就毫無用處。
與此相關(guān)的是,有狀態(tài)的對象有時很有用,甚至是必要的,但如果能用函數(shù)完成的事情,就使用函數(shù)。NPM上的一些INI文件讀取器提供了一種接口風(fēng)格,要求你首先創(chuàng)建一個對象,然后將文件加載到該對象中,最后使用特定方法獲取結(jié)果。這種方式在面向?qū)ο缶幊虃鹘y(tǒng)中很常見,但卻很糟糕。你不是簡單地調(diào)用一個函數(shù)就完成任務(wù),而是必須按部就班地讓對象經(jīng)歷各種狀態(tài)。而且由于數(shù)據(jù)現(xiàn)在被封裝在一種特定的對象類型中,所有與之交互的代碼都必須了解該類型,從而產(chǎn)生了不必要的相互依賴。
通常,定義新的數(shù)據(jù)結(jié)構(gòu)是無法避免的——語言標準只提供了少數(shù)幾種基本數(shù)據(jù)結(jié)構(gòu),而許多類型的數(shù)據(jù)必須比數(shù)組或映射更復(fù)雜。但如果數(shù)組就足夠了,那就使用數(shù)組。
稍微復(fù)雜一點的數(shù)據(jù)結(jié)構(gòu)的一個例子是第7章中的圖。在JavaScript中,沒有一種單一的、顯而易見的方式來表示圖。在那一章中,我們使用了一個對象,其屬性值是字符串?dāng)?shù)組——表示從該節(jié)點可到達的其他節(jié)點。
NPM上有幾個不同的路徑查找包,但它們都沒有使用這種圖的表示格式。它們通常允許圖的邊帶有權(quán)重,即與之相關(guān)的成本或距離。在我們的表示方式中這是不可能實現(xiàn)的。
例如,有一個 dijkstrajs 包。一種著名的路徑查找方法,與我們的 findRoute 函數(shù)非常相似,叫做迪杰斯特拉算法(Dijkstra's algorithm),以首次提出該算法的Edsger Dijkstra命名。js 后綴通常添加到包名中,以表明它們是用JavaScript編寫的。這個 dijkstrajs 包使用了一種與我們類似的圖格式,但它不是使用數(shù)組,而是使用對象,其屬性值是數(shù)字——表示邊的權(quán)重。
如果我們想使用那個包,就必須確保我們的圖以它期望的格式存儲。由于我們簡化的模型將每條路的成本都視為相同(一次轉(zhuǎn)彎),所以所有邊的權(quán)重都相同。
const {find_path} = require("dijkstrajs");
let graph = {};
for (let node of Object.keys(roadGraph)) {
let edges = graph[node] = {};
for (let dest of roadGraph[node]) {
edges[dest] = 1;
}
}
console.log(find_path(graph, "Post Office", "Cabin"));
// → ["Post Office", "Alice's House", "Cabin"]
這段代碼使用了 dijkstrajs 包中的 find_path 函數(shù)來在一個圖中查找從 “Post Office” 到 “Cabin” 的路徑。以下是詳細解釋:
-
導(dǎo)入函數(shù):
const {find_path} = require("dijkstrajs");- 使用
require函數(shù)從dijkstrajs包中解構(gòu)導(dǎo)入find_path函數(shù)。這是CommonJS模塊系統(tǒng)中導(dǎo)入模塊功能的方式。
-
構(gòu)建圖數(shù)據(jù)結(jié)構(gòu):
let graph = {};- 初始化一個空對象
graph,用于表示符合dijkstrajs包要求格式的圖。 for (let node of Object.keys(roadGraph)) {- 遍歷
roadGraph對象的所有鍵(假設(shè)roadGraph是一個已定義的表示圖結(jié)構(gòu)的對象)。對于每個節(jié)點:let edges = graph[node] = {};- 在
graph中為當(dāng)前節(jié)點創(chuàng)建一個空的鄰接邊對象。 for (let dest of roadGraph[node]) {- 對于當(dāng)前節(jié)點在
roadGraph中的每個鄰居節(jié)點dest:edges[dest] = 1;- 在
graph中,為當(dāng)前節(jié)點與鄰居節(jié)點之間的邊賦予權(quán)重1。這意味著在這個圖中,每條邊的成本是相同的。
-
查找路徑并輸出:
console.log(find_path(graph, "Post Office", "Cabin"));- 調(diào)用
find_path函數(shù),傳入構(gòu)建好的圖graph以及起始節(jié)點 “Post Office” 和目標節(jié)點 “Cabin”。該函數(shù)會在圖中查找從起始節(jié)點到目標節(jié)點的路徑,并返回路徑數(shù)組。 - 最后使用
console.log將路徑打印到控制臺,例如["Post Office", "Alice's House", "Cabin"]就是找到的一條路徑。
這段代碼展示了如何將現(xiàn)有的圖數(shù)據(jù)結(jié)構(gòu)(roadGraph)轉(zhuǎn)換為 dijkstrajs 包所需的格式,并使用該包提供的功能來查找路徑。
這可能會成為組合的障礙——當(dāng)不同的軟件包使用不同的數(shù)據(jù)結(jié)構(gòu)來描述相似的事物時,將它們組合起來就會很困難。因此,如果你希望設(shè)計出具有可組合性的模塊,就要了解其他人正在使用哪些數(shù)據(jù)結(jié)構(gòu),并在可能的情況下,效仿他們的做法。
為一個程序設(shè)計合適的模塊結(jié)構(gòu)可能很困難。在你仍處于探索問題、嘗試不同方法以找出可行方案的階段時,或許不必過于擔(dān)心模塊結(jié)構(gòu),因為保持一切有條理可能會分散你太多精力。一旦你有了感覺可靠的成果,這時就是退后一步并進行整理的好時機。
總結(jié)
模塊通過將代碼分隔為具有清晰接口和依賴關(guān)系的部分,為大型程序提供結(jié)構(gòu)。接口是模塊對其他模塊可見的部分,而依賴則是它所使用的其他模塊。
由于JavaScript在歷史上并未提供模塊系統(tǒng),因此在其基礎(chǔ)上構(gòu)建了CommonJS系統(tǒng)。后來在某個階段,JavaScript獲得了內(nèi)置的模塊系統(tǒng),目前該系統(tǒng)與CommonJS系統(tǒng)共存,但并不融洽。
軟件包是一段可獨立分發(fā)的代碼。NPM是JavaScript軟件包的存儲庫。你可以從它那里下載各種有用(以及無用)的軟件包。
練習(xí):模塊化機器人
這是第7章項目創(chuàng)建的綁定:
roadsbuildGraphroadGraphVillageStaterunRobotrandomPickrandomRobotmailRouterouteRobotfindRoutegoalOrientedRobot
如果要將該項目編寫為模塊化程序,可以考慮以下模塊劃分:
1. 圖相關(guān)模塊
-
模塊名稱:
graphModule -
功能:負責(zé)處理圖的構(gòu)建和相關(guān)操作。包含
buildGraph函數(shù),用于根據(jù)道路數(shù)據(jù)構(gòu)建圖結(jié)構(gòu);roadGraph可以作為該模塊導(dǎo)出的一個預(yù)構(gòu)建好的圖實例。 - 依賴:無
- 接口:
// graphModule.js
function buildGraph(edges) {
// 構(gòu)建圖的邏輯
}
const roadGraph = buildGraph(/* 道路數(shù)據(jù) */);
exports.buildGraph = buildGraph;
exports.roadGraph = roadGraph;
2. 機器人運行相關(guān)模塊
-
模塊名稱:
robotRunnerModule -
功能:管理機器人的運行邏輯,包含
runRobot函數(shù),它需要使用圖結(jié)構(gòu)(從graphModule導(dǎo)入)以及機器人的行為邏輯(可能從其他模塊導(dǎo)入)來運行機器人。 -
依賴:
graphModule - 接口:
// robotRunnerModule.js
const { roadGraph } = require('./graphModule.js');
function runRobot(state, robot, memory) {
// 運行機器人的邏輯
}
exports.runRobot = runRobot;
3. 機器人行為相關(guān)模塊
-
模塊名稱:
robotBehaviorModule -
功能:定義不同機器人的行為函數(shù),如
randomRobot、routeRobot、goalOrientedRobot。這些函數(shù)可能依賴于圖結(jié)構(gòu)(從graphModule導(dǎo)入)以及路徑查找功能(可能從其他模塊導(dǎo)入)。 -
依賴:
graphModule - 接口:
// robotBehaviorModule.js
const { roadGraph } = require('./graphModule.js');
function randomRobot(state) {
// 隨機選擇路徑的機器人邏輯
}
function routeRobot(state, memory) {
// 根據(jù)固定路線運行的機器人邏輯
}
function goalOrientedRobot(state, memory) {
// 以目標為導(dǎo)向的機器人邏輯
}
exports.randomRobot = randomRobot;
exports.routeRobot = routeRobot;
exports.goalOrientedRobot = goalOrientedRobot;
4. 輔助函數(shù)模塊
-
模塊名稱:
utilityModule -
功能:包含一些輔助函數(shù),如
randomPick用于隨機選擇元素,findRoute用于在圖中查找路徑。這些函數(shù)可能依賴于圖結(jié)構(gòu)(從graphModule導(dǎo)入)。 -
依賴:
graphModule - 接口:
// utilityModule.js
const { roadGraph } = require('./graphModule.js');
function randomPick(array) {
// 隨機選擇數(shù)組元素的邏輯
}
function findRoute(graph, from, to) {
// 在圖中查找路徑的邏輯
}
exports.randomPick = randomPick;
exports.findRoute = findRoute;
5. 村莊狀態(tài)相關(guān)模塊
-
模塊名稱:
villageStateModule -
功能:定義
VillageState類,用于表示村莊的狀態(tài),包括位置、郵件等信息。 - 依賴:無
- 接口:
// villageStateModule.js
class VillageState {
constructor(place, parcels) {
// 初始化村莊狀態(tài)的邏輯
}
}
exports.VillageState = VillageState;
哪些部分可能在NPM上已預(yù)先編寫?
-
圖相關(guān)功能:圖的構(gòu)建和操作在NPM上可能有成熟的包,例如
graphlib等包可以處理圖的各種操作,可能包含類似buildGraph的功能。 -
路徑查找功能:像
dijkstrajs這樣的包提供了路徑查找算法,類似findRoute的功能可能已經(jīng)存在。
更傾向于使用NPM包還是自己編寫?
-
使用NPM包的優(yōu)勢:
- 節(jié)省時間:可以快速獲取經(jīng)過測試和優(yōu)化的代碼,減少開發(fā)時間。
- 可靠性:通常由社區(qū)維護,經(jīng)過多人使用和驗證,更可靠。
-
自己編寫的優(yōu)勢:
- 定制性:可以根據(jù)項目的具體需求進行定制化開發(fā),更好地貼合項目邏輯。
- 學(xué)習(xí)機會:有助于深入理解相關(guān)算法和功能的實現(xiàn)原理。
綜合考慮,如果項目時間緊張且對功能定制性要求不高,優(yōu)先選擇使用NPM包。如果希望深入理解原理或者項目有特殊需求,自己編寫可能是更好的選擇。
道路模塊
基于第7章的示例編寫一個ES模塊,該模塊包含道路數(shù)組,并將表示這些道路的圖數(shù)據(jù)結(jié)構(gòu)作為 roadGraph 導(dǎo)出。它依賴于一個 ./graph.js 模塊,該模塊導(dǎo)出一個 buildGraph 函數(shù),用于構(gòu)建圖。此函數(shù)需要一個由二元數(shù)組組成的數(shù)組(道路的起點和終點)作為參數(shù)。
import { buildGraph } from './graph.js';
const roads = [
"Alice's House-Bob's House",
"Alice's House-Cabin",
"Alice's House-Post Office",
"Bob's House-Town Hall",
"Daria's House-Ernie's House",
"Daria's House-Town Hall",
"Ernie's House-Grete's House",
"Grete's House-Farm",
"Grete's House-Shop",
"Marketplace-Farm",
"Marketplace-Post Office",
"Marketplace-Shop",
"Marketplace-Town Hall",
"Shop-Town Hall"
];
const roadGraph = buildGraph(roads.map(road => road.split('-')));
export { roads, roadGraph };
In this code:
- We first import the
buildGraphfunction from the./graph.jsmodule. This function is expected to build a graph from an array of two - element arrays representing the start and end points of roads. - We have the
roadsarray defined as in the problem statement. - We transform the
roadsarray into the format expected bybuildGraph(an array of two - element arrays) usingmapandsplit('-'). Then we build theroadGraphusing thebuildGraphfunction. - Finally, we export both the
roadsarray and theroadGraphobject so that other modules can use them.
循環(huán)依賴
循環(huán)依賴是指模塊A依賴于模塊B,而模塊B也直接或間接地依賴于模塊A的情況。許多模塊系統(tǒng)干脆禁止這種情況,因為無論你選擇以何種順序加載此類模塊,都無法確保每個模塊在運行前其依賴項已全部加載。
CommonJS模塊允許一種有限形式的循環(huán)依賴。只要這些模塊在完成加載之前不訪問彼此的接口,循環(huán)依賴就是可行的。
本章前面給出的require函數(shù)支持這種類型的循環(huán)依賴。你能看出它是如何處理循環(huán)的嗎?
-
require函數(shù)處理循環(huán)依賴的關(guān)鍵機制:-
緩存機制:
require函數(shù)維護了一個require.cache對象,用于存儲已經(jīng)加載過的模塊。當(dāng)加載一個模塊時,首先會檢查require.cache中是否已經(jīng)存在該模塊。 -
部分初始化:在處理循環(huán)依賴時,當(dāng)模塊A開始加載模塊B,而模塊B又依賴模塊A時,模塊A在加載模塊B之前,會先在
require.cache中為自己創(chuàng)建一個緩存條目(此時該條目對應(yīng)的模塊尚未完全初始化)。 -
延遲訪問接口:當(dāng)模塊B嘗試加載模塊A時,由于模塊A已在
require.cache中,盡管尚未完全初始化,但模塊B可以獲取到這個緩存條目(即模塊A的一個“占位符”)。模塊B不會等待模塊A完全初始化完成就繼續(xù)執(zhí)行自身的加載邏輯,只要模塊B在加載過程中不訪問模塊A的接口(直到模塊A完全加載完成),就不會出現(xiàn)問題。 - 最終完成初始化:模塊B加載完成后,模塊A繼續(xù)完成剩余的加載步驟,最終所有模塊都能正確加載并使用彼此的接口。
-
緩存機制:
例如,假設(shè)存在模塊A和模塊B,模塊A依賴模塊B,模塊B依賴模塊A:
// 模塊A
const b = require('B');
// 這里假設(shè)模塊A在后續(xù)才會使用b的接口,而不是立即使用
exports.aValue = '一些值';
// 模塊B
const a = require('A');
// 這里假設(shè)模塊B在后續(xù)才會使用a的接口,而不是立即使用
exports.bValue = '其他值';
在這種情況下,當(dāng)開始加載模塊A時:
-
require('B')會開始加載模塊B。 - 在加載模塊B時,
require('A')發(fā)現(xiàn)模塊A已經(jīng)在require.cache中(雖然未完全初始化),模塊B繼續(xù)加載自身邏輯。 - 模塊B加載完成后,模塊A繼續(xù)完成加載,最終兩個模塊都能正常使用彼此導(dǎo)出的接口。
-
總結(jié):
- 總之,
require函數(shù)通過require.cache中的緩存機制,允許模塊在未完全初始化時就被其他模塊引用,只要模塊在加載過程中避免過早訪問彼此未初始化完成的接口,就能處理循環(huán)依賴的情況。這種方式為開發(fā)者提供了一定的靈活性,使得在某些場景下可以實現(xiàn)循環(huán)依賴,但也要求開發(fā)者小心編寫代碼,確保模塊之間的加載順序和接口訪問時機不會導(dǎo)致錯誤。
(出自原文https://eloquentjavascript.net/10_modules.html)
- 總之,