j.s模塊

模塊
編寫易于刪除但不易擴展的代碼。
以下是一個用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),就無法修改其行為。

在更面向?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 支持兩種不同類型的程序。腳本的行為方式依舊沿用舊有模式:它們的綁定在全局作用域中定義,且無法直接引用其他腳本。而模塊擁有自己獨立的作用域,并支持 importexport 關(guān)鍵字(腳本中無法使用這兩個關(guān)鍵字),用于聲明其依賴關(guān)系和接口。這種模塊系統(tǒng)通常被稱為 ES 模塊(其中 ES 代表 ECMAScript)。

一個模塊化程序由許多這樣的模塊組成,這些模塊通過導(dǎo)入(import)和導(dǎo)出(export)相互連接。

以下示例模塊用于在日期名稱和數(shù)字(如 DategetDay 方法返回的數(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ù) dayNamedayNumber 。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é)果打印到控制臺。

以下是詳細解釋:

  1. import {dayName} from "./dayname.js";
    • 這行代碼使用ES模塊的import語句,從當(dāng)前目錄下的dayname.js文件中導(dǎo)入dayName函數(shù)。{}用于指定要導(dǎo)入的具體綁定(這里是dayName函數(shù))。
  2. let now = new Date();
    • 創(chuàng)建一個Date對象now,它代表當(dāng)前的日期和時間。
  3. 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)入它們的模塊使用。

importexport聲明不能出現(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格式的字符串。

  1. import {parse} from "ini";
    • 這里使用ES模塊的 import 語句,從名為 ini 的包中導(dǎo)入 parse 函數(shù)。在JavaScript中,當(dāng)從包中導(dǎo)入模塊時,不需要像導(dǎo)入本地模塊那樣指定文件路徑。ini 包是一個在NPM上可用的包,提供了處理INI文件格式的功能。
  2. 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對象,其中鍵 xy 分別對應(yīng)其在INI字符串中的值。

這段代碼展示了如何利用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)了類似模塊的封裝效果。以下是詳細解釋:

  1. 定義模塊函數(shù)并立即調(diào)用
    • const weekDay = function() { ... }();
    • 這里定義了一個匿名函數(shù),然后通過末尾的 () 立即調(diào)用它,并將返回值賦值給 weekDay 變量。這個匿名函數(shù)內(nèi)部的代碼就像是模塊內(nèi)部的代碼,具有自己的局部作用域。
  2. 模塊內(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ù)返回一個對象,這個對象包含兩個方法 namenumber。這兩個方法就構(gòu)成了類似模塊的接口,外部代碼可以通過 weekDay 變量訪問到這兩個方法,而 names 數(shù)組對外部是隱藏的。
  3. 使用模塊功能
    • console.log(weekDay.name(weekDay.number("Sunday")));
    • 首先調(diào)用 weekDay.number("Sunday"),這個方法會返回 Sundaynames 數(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模塊,用于格式化日期。以下是對代碼的詳細解釋:

  1. 引入依賴模塊

    • 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(用于獲取月份的名稱),并分別賦值給同名變量。
  2. 定義導(dǎo)出函數(shù)

    • exports.formatDate = function(date, format) {... };:在exports對象上定義一個名為formatDate的函數(shù),這是該模塊對外暴露的接口。這個函數(shù)接受兩個參數(shù):date(一個Date對象)和format(一個用于指定日期格式的模板字符串)。
  3. 日期格式化邏輯

    • 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ù)進行處理。
  4. 具體標簽替換邏輯

    • 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)容的對象 —— daysmonths 是名稱數(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ù)來格式化日期。

  1. 導(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。
  2. 調(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ù)中(賦予其自身的局部作用域),并將requireexports綁定作為參數(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模塊加載機制。以下是對代碼的詳細解釋:

  1. 檢查緩存
    • if (!(name in require.cache)) {... }require.cache 是一個對象,用于存儲已經(jīng)加載過的模塊。這行代碼檢查名為 name 的模塊是否已經(jīng)在緩存中。如果不在緩存中,則執(zhí)行后續(xù)的加載操作。
  2. 讀取模塊代碼
    • let code = readFile(name);:假設(shè)存在 readFile 函數(shù),它根據(jù)模塊名 name 讀取相應(yīng)文件的內(nèi)容,并將內(nèi)容賦值給 code 變量。這里的 readFile 函數(shù)是自定義的,在實際實現(xiàn)中應(yīng)提供從文件系統(tǒng)讀取文件的邏輯。
  3. 初始化緩存和導(dǎo)出對象
    • let exports = require.cache[name] = {};:在 require.cache 中為當(dāng)前模塊創(chuàng)建一個緩存條目,并初始化一個空的 exports 對象。這個 exports 對象將用于存儲模塊導(dǎo)出的內(nèi)容。
  4. 創(chuàng)建包裝函數(shù)
    • let wrapper = Function("require, exports", code);:使用 Function 構(gòu)造函數(shù)創(chuàng)建一個新的函數(shù) wrapper。這個函數(shù)接受 requireexports 作為參數(shù),函數(shù)體是從文件中讀取的模塊代碼 code。這相當(dāng)于將模塊代碼包裝在一個函數(shù)中,為模塊提供了自己的局部作用域。
  5. 執(zhí)行包裝函數(shù)
    • wrapper(require, exports);:執(zhí)行 wrapper 函數(shù),并傳入 requireexports 對象。模塊代碼在執(zhí)行過程中,可以通過 require 引入其他模塊,通過向 exports 對象添加屬性來導(dǎo)出內(nèi)容。
  6. 返回緩存中的模塊
    • return require.cache[name];:無論模塊是否已經(jīng)在緩存中,最后都返回 require.cache 中對應(yīng)模塊的導(dǎo)出內(nèi)容。
  7. 初始化緩存對象
    • 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ù)。

通過將 requireexports 定義為生成的包裝函數(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對象,提供了 parsestringify(用于編寫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” 的路徑。以下是詳細解釋:

  1. 導(dǎo)入函數(shù)

    • const {find_path} = require("dijkstrajs");
    • 使用 require 函數(shù)從 dijkstrajs 包中解構(gòu)導(dǎo)入 find_path 函數(shù)。這是CommonJS模塊系統(tǒng)中導(dǎo)入模塊功能的方式。
  2. 構(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。這意味著在這個圖中,每條邊的成本是相同的。
  3. 查找路徑并輸出

    • 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)建的綁定:

  • roads
  • buildGraph
  • roadGraph
  • VillageState
  • runRobot
  • randomPick
  • randomRobot
  • mailRoute
  • routeRobot
  • findRoute
  • goalOrientedRobot

如果要將該項目編寫為模塊化程序,可以考慮以下模塊劃分:

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、routeRobotgoalOrientedRobot。這些函數(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:

  1. We first import the buildGraph function from the ./graph.js module. This function is expected to build a graph from an array of two - element arrays representing the start and end points of roads.
  2. We have the roads array defined as in the problem statement.
  3. We transform the roads array into the format expected by buildGraph (an array of two - element arrays) using map and split('-'). Then we build the roadGraph using the buildGraph function.
  4. Finally, we export both the roads array and the roadGraph object 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)的嗎?

  1. 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)出的接口。
  1. 總結(jié)
    • 總之,require函數(shù)通過require.cache中的緩存機制,允許模塊在未完全初始化時就被其他模塊引用,只要模塊在加載過程中避免過早訪問彼此未初始化完成的接口,就能處理循環(huán)依賴的情況。這種方式為開發(fā)者提供了一定的靈活性,使得在某些場景下可以實現(xiàn)循環(huán)依賴,但也要求開發(fā)者小心編寫代碼,確保模塊之間的加載順序和接口訪問時機不會導(dǎo)致錯誤。
      (出自原文https://eloquentjavascript.net/10_modules.html
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容