第五章:作用域閉包

特別說(shuō)明,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS

希望我們是帶著對(duì)作用域工作方式的健全,堅(jiān)實(shí)的理解來(lái)到這里的。

我們將我們的注意力轉(zhuǎn)向這個(gè)語(yǔ)言中一個(gè)重要到不可思議,但是一直難以捉摸的、幾乎是神話般的 部分:閉包。如果你至此一直跟隨著我們關(guān)于詞法作用域的討論,那么你會(huì)感覺(jué)閉包將在很大程度上沒(méi)那么令人激動(dòng),幾乎是顯而易見(jiàn)的。有一個(gè)魔法師坐在幕后,現(xiàn)在我們即將見(jiàn)到他。不,他的名字不是 Crockford!

如果你還對(duì)詞法作用域感到不安,那么現(xiàn)在就是在繼續(xù)之前回過(guò)頭去再?gòu)?fù)習(xí)一下第二章的好時(shí)機(jī)。

啟蒙

對(duì)于那些對(duì) JavaScript 有些經(jīng)驗(yàn),但是也許從沒(méi)全面掌握閉包概念的人來(lái)說(shuō),理解閉包 看起來(lái)就像是必須努力并作出犧牲才能到達(dá)的涅槃狀態(tài)。

回想幾年前我對(duì) JavaScript 有了牢固的掌握,但是不知道閉包是什么。它暗示著這種語(yǔ)言有著另外的一面,它許諾了甚至比我已經(jīng)擁有的還多的力量,它取笑并嘲弄我。我記得我通讀早期框架的源代碼試圖搞懂它到底是如何工作的。我記得第一次“模塊模式”的某些東西融入我的大腦。我記得那依然栩栩如生的 啊哈! 一刻。

那時(shí)我不明白的東西,那個(gè)花了我好幾年時(shí)間才搞懂的東西,那個(gè)我即將傳授給你的東西,是這個(gè)秘密:在 JavaScript 中閉包無(wú)所不在,你只是必須認(rèn)出它并接納它。閉包不是你必須學(xué)習(xí)新的語(yǔ)法和模式才能使用的特殊的可選的工具。不,閉包甚至不是你必須像盧克在原力中修煉那樣,一定要學(xué)會(huì)使用并掌握的武器。

閉包是依賴于詞法作用域編寫(xiě)代碼而產(chǎn)生的結(jié)果。它們就這么發(fā)生了。要利用它們你甚至不需要有意地創(chuàng)建閉包。閉包在你的代碼中一直在被創(chuàng)建和使用。你 缺少 的是恰當(dāng)?shù)乃季S環(huán)境,來(lái)識(shí)別,接納,并以自己的意志利用閉包。

啟蒙的時(shí)刻應(yīng)該是:哦,閉包已經(jīng)在我的代碼中到處發(fā)生了,現(xiàn)在我終于 看到 它們了。理解閉包就像是尼歐第一次見(jiàn)到母體。

事實(shí)真相

好了,夸張和對(duì)電影的無(wú)恥引用夠多了。

為了理解和識(shí)別閉包,這里有一個(gè)你需要知道的簡(jiǎn)單粗暴的定義:

閉包就是函數(shù)能夠記住并訪問(wèn)它的詞法作用域,即使當(dāng)這個(gè)函數(shù)在它的詞法作用域之外執(zhí)行時(shí)。

讓我們跳進(jìn)代碼來(lái)說(shuō)明這個(gè)定義:

function foo() {
    var a = 2;

    function bar() {
        console.log( a ); // 2
    }

    bar();
}

foo();

根據(jù)我們對(duì)嵌套作用域的討論,這段代碼應(yīng)當(dāng)看起來(lái)很熟悉。由于詞法作用域查詢規(guī)則(在這個(gè)例子中,是一個(gè) RHS 引用查詢),函數(shù) bar() 可以 訪問(wèn) 外圍作用域的變量 a

這是“閉包”嗎?

好吧,從技術(shù)上講…… 也許是。但是根據(jù)我們上面的“你需要知道”的定義…… 不確切。我認(rèn)為解釋 bar() 引用 a 的最準(zhǔn)確的方式是根據(jù)詞法作用域查詢規(guī)則,但是那些規(guī)則 僅僅 是閉包的(一個(gè)很重要的!)一部分。

從純粹的學(xué)院派角度講,上面的代碼段被認(rèn)為是函數(shù) bar() 在函數(shù) foo() 的作用域上有一個(gè) 閉包(而且實(shí)際上,它甚至對(duì)其他的作用域也可以訪問(wèn),比如這個(gè)例子中的全局作用域)。換一種略有不同的說(shuō)法是,bar() 閉住了 foo() 的作用域。為什么?因?yàn)?bar() 嵌套地出現(xiàn)在 foo() 內(nèi)部。就這么簡(jiǎn)單。

但是,這樣一來(lái)閉包的定義就是不能直接 觀察到 的了,我們也不能看到閉包在這個(gè)代碼段中 被行使。我們清楚地看到詞法作用域,但是閉包仍然像代碼后面謎一般的模糊陰影。

讓我們考慮這段將閉包完全帶到聚光燈下的代碼:

function foo() {
    var a = 2;

    function bar() {
        console.log( a );
    }

    return bar;
}

var baz = foo();

baz(); // 2 -- 哇噢,看到閉包了,伙計(jì)。

函數(shù) bar() 對(duì)于 foo() 內(nèi)的作用域擁有詞法作用域訪問(wèn)權(quán)。但是之后,我們拿起 bar(),這個(gè)函數(shù)本身,將它像 一樣傳遞。在這個(gè)例子中,我們 return bar 引用的函數(shù)對(duì)象本身。

在執(zhí)行 foo() 之后,我們將它返回的值(我們的內(nèi)部 bar() 函數(shù))賦予一個(gè)稱為 baz 的變量,然后我們實(shí)際地調(diào)用 baz(),這將理所當(dāng)然地調(diào)用我們內(nèi)部的函數(shù) bar(),只不過(guò)是通過(guò)一個(gè)不同的標(biāo)識(shí)符引用。

bar() 被執(zhí)行了,必然的。但是在這個(gè)例子中,它是在它被聲明的詞法作用域 外部 被執(zhí)行的。

foo() 被執(zhí)行之后,一般說(shuō)來(lái)我們會(huì)期望 foo() 的整個(gè)內(nèi)部作用域都將消失,因?yàn)槲覀冎?引擎 啟用了 垃圾回收器 在內(nèi)存不再被使用時(shí)來(lái)回收它們。因?yàn)楹茱@然 foo() 的內(nèi)容不再被使用了,所以看起來(lái)它們很自然地應(yīng)該被認(rèn)為是 消失了。

但是閉包的“魔法”不會(huì)讓這發(fā)生。內(nèi)部的作用域?qū)嶋H上 依然 “在使用”,因此將不會(huì)消失。誰(shuí)在使用它?函數(shù) bar() 本身。

有賴于它被聲明的位置,bar() 擁有一個(gè)詞法作用域閉包覆蓋著 foo() 的內(nèi)部作用域,閉包為了能使 bar() 在以后任意的時(shí)刻可以引用這個(gè)作用域而保持它的存在。

bar() 依然擁有對(duì)那個(gè)作用域的引用,而這個(gè)引用稱為閉包。

所以,在幾微秒之后,當(dāng)變量 baz 被調(diào)用時(shí)(調(diào)用我們最開(kāi)始標(biāo)記為 bar 的內(nèi)部函數(shù)),它理所應(yīng)當(dāng)?shù)貙?duì)編寫(xiě)時(shí)的詞法作用域擁有 訪問(wèn) 權(quán),所以它可以如我們所愿地訪問(wèn)變量 a。

這個(gè)函數(shù)在它被編寫(xiě)時(shí)的詞法作用域之外被調(diào)用。閉包 使這個(gè)函數(shù)可以繼續(xù)訪問(wèn)它在編寫(xiě)時(shí)被定義的詞法作用域。

當(dāng)然,函數(shù)可以被作為值傳遞,而且實(shí)際上在其他位置被調(diào)用的所有各種方式,都是觀察/行使閉包的例子。

function foo() {
    var a = 2;

    function baz() {
        console.log( a ); // 2
    }

    bar( baz );
}

function bar(fn) {
    fn(); // 看媽媽,我看到閉包了!
}

我們將內(nèi)部函數(shù) baz 傳遞給 bar,并調(diào)用這個(gè)內(nèi)部函數(shù)(現(xiàn)在被標(biāo)記為 fn),當(dāng)我們這么做時(shí),它覆蓋在 foo() 內(nèi)部作用域的閉包就可以通過(guò) a 的訪問(wèn)觀察到。

這樣的函數(shù)傳遞也可以是間接的。

var fn;

function foo() {
    var a = 2;

    function baz() {
        console.log( a );
    }

    fn = baz; // 將`baz`賦值給一個(gè)全局變量
}

function bar() {
    fn(); // 看媽媽,我看到閉包了!
}

foo();

bar(); // 2

無(wú)論我們使用什么方法將內(nèi)部函數(shù) 傳送 到它的詞法作用域之外,它都將維護(hù)一個(gè)指向它最開(kāi)始被聲明時(shí)的作用域的引用,而且無(wú)論我們什么時(shí)候執(zhí)行它,這個(gè)閉包就會(huì)被行使。

現(xiàn)在我能看到了

前面的代碼段有些學(xué)術(shù)化,而且是人工構(gòu)建來(lái)說(shuō)明 閉包的使用 的。但我保證過(guò)給你的東西不止是一個(gè)新的酷玩具。我保證過(guò)閉包是在你的現(xiàn)存代碼中無(wú)處不在的東西?,F(xiàn)在讓我們 看看 真相。

function wait(message) {

    setTimeout( function timer(){
        console.log( message );
    }, 1000 );

}

wait( "Hello, closure!" );

我們拿來(lái)一個(gè)內(nèi)部函數(shù)(名為 timer)將它傳遞給 setTimeout(..)。但是 timer 擁有覆蓋 wait(..) 的作用域的閉包,實(shí)際上保持并使用著對(duì)變量 message 的引用。

在我們執(zhí)行 wait(..) 一千毫秒之后,要不是內(nèi)部函數(shù) timer 依然擁有覆蓋著 wait() 內(nèi)部作用域的閉包,它早就會(huì)消失了。

引擎 的內(nèi)臟深處,內(nèi)建的工具 setTimeout(..) 擁有一些參數(shù)的引用,可能稱為 fn 或者 func 或者其他諸如此類的東西。引擎 去調(diào)用這個(gè)函數(shù),它調(diào)用我們的內(nèi)部 timer 函數(shù),而詞法作用域依然完好無(wú)損。

閉包。

或者,如果你信仰jQuery(或者就此而言,其他的任何JS框架):

function setupBot(name,selector) {
    $( selector ).click( function activator(){
        console.log( "Activating: " + name );
    } );
}

setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );

我不確定你寫(xiě)的是什么代碼,但我通常寫(xiě)一些代碼來(lái)負(fù)責(zé)控制全球的閉包無(wú)人機(jī)軍團(tuán),所以這完全是真實(shí)的!

把玩笑放在一邊,實(shí)質(zhì)上 無(wú)論何時(shí)何地 只要你將函數(shù)作為頭等的值看待并將它們傳來(lái)傳去的話,你就可能看到這些函數(shù)行使閉包。計(jì)時(shí)器、事件處理器、Ajax請(qǐng)求、跨窗口消息、web worker、或者任何其他的異步(或同步!)任務(wù),當(dāng)你傳入一個(gè) 回調(diào)函數(shù),你就在它周?chē)鷳覓炝艘恍╅]包!

注意: 第三章介紹了 IIFE 模式。雖然人們常說(shuō) IIFE(獨(dú)自)是一個(gè)可以觀察到閉包的例子,但是根據(jù)我們上面的定義,我有些不同意。

var a = 2;

(function IIFE(){
    console.log( a );
})();

這段代碼“好用”,但嚴(yán)格來(lái)說(shuō)它不是在觀察閉包。為什么?因?yàn)檫@個(gè)函數(shù)(就是我們這里命名為“IIFE”的那個(gè))沒(méi)有在它的詞法作用域之外執(zhí)行。它仍然在它被聲明的相同作用域中(那個(gè)同時(shí)持有 a 的外圍/全局作用域)被調(diào)用。a 是通過(guò)普通的詞法作用域查詢找到的,不是通過(guò)真正的閉包。

雖說(shuō)技術(shù)上閉包可能發(fā)生在聲明時(shí),但它 不是 嚴(yán)格地可以觀察到的,因此,就像人們說(shuō)的,它是一顆在森林中倒掉的樹(shù),但周?chē)鷽](méi)人去聽(tīng)到它。

雖然 IIFE 本身 不是一個(gè)閉包的例子,但是它絕對(duì)創(chuàng)建了作用域,而且它是我們用來(lái)創(chuàng)建可以被閉包的作用域的最常見(jiàn)工具之一。所以 IIFE 確實(shí)與閉包有強(qiáng)烈的關(guān)聯(lián),即便它們本身不行使閉包。

親愛(ài)的讀者,現(xiàn)在把這本書(shū)放下。我有一個(gè)任務(wù)給你。去打開(kāi)一些你最近的 JavaScript 代碼。尋找那些被你作為值的函數(shù),并識(shí)別你已經(jīng)在那里使用了閉包,而你以前甚至可能不知道它。

我會(huì)等你。

現(xiàn)在……你看到了!

循環(huán) + 閉包

用來(lái)展示閉包最常見(jiàn)最權(quán)威的例子是老實(shí)巴交的 for 循環(huán)。

for (var i=1; i<=5; i++) {
    setTimeout( function timer(){
        console.log( i );
    }, i*1000 );
}

注意: 當(dāng)你將函數(shù)放在循環(huán)內(nèi)部時(shí) Linter 經(jīng)常會(huì)抱怨,因?yàn)椴焕斫忾]包的錯(cuò)誤 在開(kāi)發(fā)者中太常見(jiàn)了。我們?cè)谶@里講解如何正確地利用閉包的全部力量。但是 Linter 通常不理解這樣的微妙之處,所以它們不管怎樣都將抱怨,認(rèn)為你 實(shí)際上 不知道你在做什么。

這段代碼的精神是,我們一般將 期待 它的行為是分別打印數(shù)字“1”,“2”,……“5”,一次一個(gè),一秒一個(gè)。

實(shí)際上,如果你運(yùn)行這段代碼,你會(huì)得到“6”被打印5次,一秒一個(gè)。

???

首先,讓我們解釋一下“6”是從哪兒來(lái)的。循環(huán)的終結(jié)條件是 i <=5。第一次滿足這個(gè)條件時(shí) i 是6。所以,輸出的結(jié)果反映的是 i 在循環(huán)終結(jié)后的最終值。

如果多看兩眼的話這其實(shí)很明顯。超時(shí)的回調(diào)函數(shù)都將在循環(huán)的完成之后立即運(yùn)行。實(shí)際上,就計(jì)時(shí)器而言,即便在每次迭代中它是 setTimeout(.., 0),所有這些回調(diào)函數(shù)也都仍然是嚴(yán)格地在循環(huán)之后運(yùn)行的,因此每次都打印 6

但是這里有個(gè)更深刻的問(wèn)題。要是想讓它實(shí)際上如我們?cè)谡Z(yǔ)義上暗示的那樣動(dòng)作,我們的代碼缺少了什么?

缺少的東西是,我們?cè)噲D 暗示 在迭代期間,循環(huán)的每次迭代都“捕捉”一份對(duì) i 的拷貝。但是,雖然所有這5個(gè)函數(shù)在每次循環(huán)迭代中分離地定義,由于作用域的工作方式,它們 都閉包在同一個(gè)共享的全局作用域上,而它事實(shí)上只有一個(gè) i

這么說(shuō)來(lái),所有函數(shù)共享一個(gè)指向相同的 i 的引用是 理所當(dāng)然 的。循環(huán)結(jié)構(gòu)的某些東西往往迷惑我們,使我們認(rèn)為這里有其他更精巧的東西在工作。但是這里沒(méi)有。這與根本沒(méi)有循環(huán),5個(gè)超時(shí)回調(diào)僅僅一個(gè)接一個(gè)地被聲明沒(méi)有區(qū)別。

好了,那么,回到我們火燒眉毛的問(wèn)題。缺少了什么?我們需要更多 鈴聲 被閉包的作用域。明確地說(shuō),我們需要為循環(huán)的每次迭代都準(zhǔn)備一個(gè)新的被閉包的作用域。

我們?cè)诘谌轮袑W(xué)到,IIFE 通過(guò)聲明并立即執(zhí)行一個(gè)函數(shù)來(lái)創(chuàng)建作用域。

讓我們?cè)囋嚕?/p>

for (var i=1; i<=5; i++) {
    (function(){
        setTimeout( function timer(){
            console.log( i );
        }, i*1000 );
    })();
}

這好用嗎?試試。我還會(huì)等你。

我來(lái)為你終結(jié)懸念。不好用。 但是為什么?很明顯我們現(xiàn)在有了更多的詞法作用域。每個(gè)超時(shí)回調(diào)函數(shù)確實(shí)閉包在每次迭代時(shí)分別被每個(gè) IIFE 創(chuàng)建的作用域中。

擁有一個(gè)被閉包的 空的作用域 是不夠的。仔細(xì)觀察。我們的 IIFE 只是一個(gè)空的什么也不做的作用域。它內(nèi)部需要 一些東西 才能變得對(duì)我們有用。

它需要它自己的變量,在每次迭代時(shí)持有值 i 的一個(gè)拷貝。

for (var i=1; i<=5; i++) {
    (function(){
        var j = i;
        setTimeout( function timer(){
            console.log( j );
        }, j*1000 );
    })();
}

萬(wàn)歲!它好用了!

有些人偏好一種稍稍變形的形式:

for (var i=1; i<=5; i++) {
    (function(j){
        setTimeout( function timer(){
            console.log( j );
        }, j*1000 );
    })( i );
}

當(dāng)然,因?yàn)檫@些 IIFE 只是函數(shù),我們可以傳入 i,如果我們樂(lè)意的話可以稱它為 j,或者我們甚至可以再次稱它為 i。不管哪種方式,這段代碼都能工作。

在每次迭代內(nèi)部使用的 IIFE 為每次迭代創(chuàng)建了新的作用域,這給了我們的超時(shí)回調(diào)函數(shù)一個(gè)機(jī)會(huì),在每次迭代時(shí)閉包一個(gè)新的作用域,這些作用域中的每一個(gè)都擁有一個(gè)持有正確的迭代值的變量給我們?cè)L問(wèn)。

問(wèn)題解決了!

重溫塊兒作用域

仔細(xì)觀察我們前一個(gè)解決方案的分析。我們使用了一個(gè) IIFE 來(lái)在每一次迭代中創(chuàng)建新的作用域。換句話說(shuō),我們實(shí)際上每次迭代都 需要 一個(gè) 塊兒作用域。我們?cè)诘谌抡故玖?let 聲明,它劫持一個(gè)塊兒并且就在這個(gè)塊兒中聲明一個(gè)變量。

這實(shí)質(zhì)上將塊兒變成了一個(gè)我們可以閉包的作用域。所以接下來(lái)的牛逼代碼“就是好用”:

for (var i=1; i<=5; i++) {
    let j = i; // 呀,給閉包的塊兒作用域!
    setTimeout( function timer(){
        console.log( j );
    }, j*1000 );
}

但是,這還不是全部!(用我最棒的 Bob Barker 嗓音)在用于 for 循環(huán)頭部的 let 聲明被定義了一種特殊行為。這種行為說(shuō),這個(gè)變量將不是只為循環(huán)聲明一次,而是為每次迭代聲明一次。并且,它將在每次后續(xù)的迭代中被上一次迭代末尾的值初始化。

for (let i=1; i<=5; i++) {
    setTimeout( function timer(){
        console.log( i );
    }, i*1000 );
}

這有多酷?塊兒作用域和閉包攜手工作,解決世界上所有的問(wèn)題。我不知道你怎么樣,但這使我成了一個(gè)快樂(lè)的 JavaScript 開(kāi)發(fā)者。

模塊

還有其他的代碼模式利用了閉包的力量,但是它們都不像回調(diào)那樣浮于表面。讓我們來(lái)檢視它們中最強(qiáng)大的一種:模塊。

function foo() {
    var something = "cool";
    var another = [1, 2, 3];

    function doSomething() {
        console.log( something );
    }

    function doAnother() {
        console.log( another.join( " ! " ) );
    }
}

就現(xiàn)在這段代碼來(lái)說(shuō),沒(méi)有發(fā)生明顯的閉包。我們只是擁有一些私有數(shù)據(jù)變量 somethinganother,以及幾個(gè)內(nèi)部函數(shù) doSomething()doAnother(),它們都擁有覆蓋在 foo() 內(nèi)部作用域上的詞法作用域(因此是閉包?。?。

但是現(xiàn)在考慮這段代碼:

function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];

    function doSomething() {
        console.log( something );
    }

    function doAnother() {
        console.log( another.join( " ! " ) );
    }

    return {
        doSomething: doSomething,
        doAnother: doAnother
    };
}

var foo = CoolModule();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

在 JavaScript 中我們稱這種模式為 模塊。實(shí)現(xiàn)模塊模式的最常見(jiàn)方法經(jīng)常被稱為“揭示模塊”,它是我們?cè)谶@里展示的方式的變種。

讓我們檢視關(guān)于這段代碼的一些事情。

首先,CoolModule() 只是一個(gè)函數(shù),但它 必須被調(diào)用 才能成為一個(gè)被創(chuàng)建的模塊實(shí)例。沒(méi)有外部函數(shù)的執(zhí)行,內(nèi)部作用域的創(chuàng)建和閉包都不會(huì)發(fā)生。

第二,CoolModule() 函數(shù)返回一個(gè)對(duì)象,通過(guò)對(duì)象字面量語(yǔ)法 { key: value, ... } 標(biāo)記。這個(gè)我們返回的對(duì)象擁有指向我們內(nèi)部函數(shù)的引用,但是 沒(méi)有 指向我們內(nèi)部數(shù)據(jù)變量的引用。我們可以將它們保持為隱藏和私有的。可以很恰當(dāng)?shù)卣J(rèn)為這個(gè)返回值對(duì)象實(shí)質(zhì)上是一個(gè) 我們模塊的公有API。

這個(gè)返回值對(duì)象最終被賦值給外部變量 foo,然后我們可以在這個(gè)API上訪問(wèn)那些屬性,比如 foo.doSomething()

注意: 從我們的模塊中返回一個(gè)實(shí)際的對(duì)象(字面量)不是必須的。我們可以僅僅直接返回一個(gè)內(nèi)部函數(shù)。jQuery 就是一個(gè)很好地例子。jQuery$ 標(biāo)識(shí)符是 jQuery “模塊”的公有API,但是它們本身只是一個(gè)函數(shù)(這個(gè)函數(shù)本身可以有屬性,因?yàn)樗械暮瘮?shù)都是對(duì)象)。

doSomething()doAnother() 函數(shù)擁有模塊“實(shí)例”內(nèi)部作用域的閉包(通過(guò)實(shí)際調(diào)用 CoolModule() 得到的)。當(dāng)我們通過(guò)返回值對(duì)象的屬性引用,將這些函數(shù)傳送到詞法作用域外部時(shí),我們就建立好了可以觀察和行使閉包的條件。

更簡(jiǎn)單地說(shuō),行使模塊模式有兩個(gè)“必要條件”:

  1. 必須有一個(gè)外部的外圍函數(shù),而且它必須至少被調(diào)用一次(每次創(chuàng)建一個(gè)新的模塊實(shí)例)。

  2. 外圍的函數(shù)必須至少返回一個(gè)內(nèi)部函數(shù),這樣這個(gè)內(nèi)部函數(shù)才擁有私有作用域的閉包,并且可以訪問(wèn)和/或修改這個(gè)私有狀態(tài)。

一個(gè)僅帶有一個(gè)函數(shù)屬性的對(duì)象不是 真正 的模塊。從可觀察的角度來(lái)說(shuō),一個(gè)從函數(shù)調(diào)用中返回的對(duì)象,僅帶有數(shù)據(jù)屬性而沒(méi)有閉包的函數(shù),也不是 真正 的模塊。

上面的代碼段展示了一個(gè)稱為 CoolModule() 獨(dú)立的模塊創(chuàng)建器,它可以被調(diào)用任意多次,每次創(chuàng)建一個(gè)新的模塊實(shí)例。這種模式的一個(gè)稍稍的變化是當(dāng)你只想要一個(gè)實(shí)例的時(shí)候,某種“單例”:

var foo = (function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];

    function doSomething() {
        console.log( something );
    }

    function doAnother() {
        console.log( another.join( " ! " ) );
    }

    return {
        doSomething: doSomething,
        doAnother: doAnother
    };
})();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

這里,我們將模塊放進(jìn)一個(gè) IIFE(見(jiàn)第三章)中,而且我們 立即 調(diào)用它,并把它的返回值直接賦值給我們單獨(dú)的模塊實(shí)例標(biāo)識(shí)符 foo。

模塊只是函數(shù),所以它們可以接收參數(shù):

function CoolModule(id) {
    function identify() {
        console.log( id );
    }

    return {
        identify: identify
    };
}

var foo1 = CoolModule( "foo 1" );
var foo2 = CoolModule( "foo 2" );

foo1.identify(); // "foo 1"
foo2.identify(); // "foo 2"

另一種在模塊模式上微小但是強(qiáng)大的變化是,為你作為公有API返回的對(duì)象命名:

var foo = (function CoolModule(id) {
    function change() {
        // 修改公有 API
        publicAPI.identify = identify2;
    }

    function identify1() {
        console.log( id );
    }

    function identify2() {
        console.log( id.toUpperCase() );
    }

    var publicAPI = {
        change: change,
        identify: identify1
    };

    return publicAPI;
})( "foo module" );

foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE

通過(guò)在模塊實(shí)例內(nèi)部持有一個(gè)指向公有API對(duì)象的內(nèi)部引用,你可以 從內(nèi)部 修改這個(gè)模塊,包括添加和刪除方法,屬性, 改變它們的值。

現(xiàn)代的模塊

各種模塊依賴加載器/消息機(jī)制實(shí)質(zhì)上都是將這種模塊定義包裝進(jìn)一個(gè)友好的API。與其檢視任意一個(gè)特定的庫(kù),不如讓我 (僅)為了說(shuō)明的目的 展示一個(gè) 非常簡(jiǎn)單 的概念證明:

var MyModules = (function Manager() {
    var modules = {};

    function define(name, deps, impl) {
        for (var i=0; i<deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply( impl, deps );
    }

    function get(name) {
        return modules[name];
    }

    return {
        define: define,
        get: get
    };
})();

這段代碼的關(guān)鍵部分是 modules[name] = impl.apply(impl, deps)。這為一個(gè)模塊調(diào)用了它的定義的包裝函數(shù)(傳入所有依賴),并將返回值,也就是模塊的API,存儲(chǔ)到一個(gè)用名稱追蹤的內(nèi)部模塊列表中。

這里是我可能如何使用它來(lái)定義一個(gè)模塊:

MyModules.define( "bar", [], function(){
    function hello(who) {
        return "Let me introduce: " + who;
    }

    return {
        hello: hello
    };
} );

MyModules.define( "foo", ["bar"], function(bar){
    var hungry = "hippo";

    function awesome() {
        console.log( bar.hello( hungry ).toUpperCase() );
    }

    return {
        awesome: awesome
    };
} );

var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );

console.log(
    bar.hello( "hippo" )
); // Let me introduce: hippo

foo.awesome(); // LET ME INTRODUCE: HIPPO

模塊“foo”和“bar”都使用一個(gè)返回公有API的函數(shù)來(lái)定義。“foo”甚至接收一個(gè)“bar”的實(shí)例作為依賴參數(shù),并且可以因此使用它。

花些時(shí)間檢視這些代碼段,來(lái)完全理解將閉包的力量付諸實(shí)踐給我們帶來(lái)的好處。關(guān)鍵之處在于,對(duì)于模塊管理器來(lái)說(shuō)真的沒(méi)有什么特殊的“魔法”。它們只是滿足了我在上面列出的模塊模式的兩個(gè)性質(zhì):調(diào)用一個(gè)函數(shù)定義包裝器,并將它的返回值作為這個(gè)模塊的API保存下來(lái)。

換句話說(shuō),模塊就是模塊,即便你在它們上面放了一個(gè)友好的包裝工具。

未來(lái)的模塊

ES6 為模塊的概念增加了頭等的語(yǔ)法支持。當(dāng)通過(guò)模塊系統(tǒng)加載時(shí),ES6 將一個(gè)文件視為一個(gè)獨(dú)立的模塊。每個(gè)模塊可以導(dǎo)入其他的模塊或者特定的API成員,也可以導(dǎo)出它們自己的公有API成員。

注意: 基于函數(shù)的模塊不是一個(gè)可以被靜態(tài)識(shí)別的模式(編譯器可以知道的東西),所以它們的API語(yǔ)義直到運(yùn)行時(shí)才會(huì)被考慮。也就是,你實(shí)際上可以在運(yùn)行時(shí)期間修改模塊的API(參見(jiàn)早先 publicAPI 的討論)。

相比之下,ES6 模塊API是靜態(tài)的(這些API不會(huì)在運(yùn)行時(shí)改變)。因?yàn)榫幾g器知道它,它可以(也確實(shí)在這么作?。┰冢ㄎ募虞d和)編譯期間檢查一個(gè)指向被導(dǎo)入模塊的成員的引用是否 實(shí)際存在。如果API引用不存在,編譯器就會(huì)在編譯時(shí)拋出一個(gè)“早期”錯(cuò)誤,而不是等待傳統(tǒng)的動(dòng)態(tài)運(yùn)行時(shí)解決方案(和錯(cuò)誤,如果有的話)。

ES6 模塊 沒(méi)有 “內(nèi)聯(lián)”格式,它們必須被定義在一個(gè)分離的文件中(每個(gè)模塊一個(gè))。瀏覽器/引擎擁有一個(gè)默認(rèn)的“模塊加載器”(它是可以被覆蓋的,但是這超出我們?cè)诖擞懻摰姆秶?,它在模塊被導(dǎo)入時(shí)同步地加載模塊文件。

考慮這段代碼:

bar.js

function hello(who) {
    return "Let me introduce: " + who;
}

export hello;

foo.js

// 僅導(dǎo)入“bar”模塊中的`hello()`
import hello from "bar";

var hungry = "hippo";

function awesome() {
    console.log(
        hello( hungry ).toUpperCase()
    );
}

export awesome;
// 導(dǎo)入`foo`和`bar`整個(gè)模塊
module foo from "foo";
module bar from "bar";

console.log(
    bar.hello( "rhino" )
); // Let me introduce: rhino

foo.awesome(); // LET ME INTRODUCE: HIPPO

注意: 需要使用前兩個(gè)代碼片段中的內(nèi)容分別創(chuàng)建兩個(gè)分離的文件 “foo.js”“bar.js”。然后,你的程序?qū)⒓虞d/導(dǎo)入這些模塊來(lái)使用它們,就像第三個(gè)片段那樣。

import 在當(dāng)前的作用域中導(dǎo)入一個(gè)模塊的API的一個(gè)或多個(gè)成員,每個(gè)都綁定到一個(gè)變量(這個(gè)例子中是 hello)。module 將整個(gè)模塊的API導(dǎo)入到一個(gè)被綁定的變量(這個(gè)例子中是 foo,bar)。export 為當(dāng)前模塊的公有API導(dǎo)出一個(gè)標(biāo)識(shí)符(變量,函數(shù))。在一個(gè)模塊的定義中,這些操作符可以根據(jù)需要使用任意多次。

模塊文件 內(nèi)部的內(nèi)容被視為像是包圍在一個(gè)作用域閉包中,就像早先看到的使用函數(shù)閉包的模塊那樣。

復(fù)習(xí)

對(duì)于那些還蒙在鼓里的人來(lái)說(shuō),閉包就像在 JavaScript 內(nèi)部被隔離開(kāi)的魔法世界,只有很少一些最勇敢的靈魂才能到達(dá)。但是它實(shí)際上只是一個(gè)標(biāo)準(zhǔn)的,而且?guī)缀趺黠@的事實(shí) —— 我們?nèi)绾卧诤瘮?shù)即是值,而且可以被隨意傳遞的詞法作用域環(huán)境中編寫(xiě)代碼,

閉包就是當(dāng)一個(gè)函數(shù)即使是在它的詞法作用域之外被調(diào)用時(shí),也可以記住并訪問(wèn)它的詞法作用域。

如果我們不能小心地識(shí)別它們和它們的工作方式,閉包可能會(huì)絆住我們,例如在循環(huán)中。但它們也是一種極其強(qiáng)大的工具,以各種形式開(kāi)啟了像 模塊 這樣的模式。

模塊要求兩個(gè)關(guān)鍵性質(zhì):1)一個(gè)被調(diào)用的外部包裝函數(shù),來(lái)創(chuàng)建外圍作用域。2)這個(gè)包裝函數(shù)的返回值必須包含至少一個(gè)內(nèi)部函數(shù)的引用,這個(gè)函數(shù)才擁有包裝函數(shù)內(nèi)部作用域的閉包。

現(xiàn)在我們看到了閉包在我們的代碼中無(wú)處不在,而且我們有能力識(shí)別它們,并為了我們自己的利益利用它們!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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