你不懂JS:作用域與閉包 第五章:作用域閉包

官方中文版原文鏈接

感謝社區(qū)中各位的大力支持先誉,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券口予,享受所有官網(wǎng)優(yōu)惠胰坟,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取

希望我們是帶著對(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è)很重要的G4ァ)一部分淮悼。

從純粹的學(xué)院派角度講,上面的代碼段被認(rèn)為是函數(shù)bar()在函數(shù)foo()的作用域上有一個(gè) 閉包(而且實(shí)際上揽思,它甚至對(duì)其他的作用域也可以訪問(wèn)袜腥,比如這個(gè)例子中的全局作用域)。換一種略有不同的說(shuō)法是钉汗,bar()閉住了foo()的作用域羹令。為什么?因?yàn)?code>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()之后守伸,我們將它返回的值(我們里面的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ú)處不在的東西∷鳎現(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,或者任何其他的異步(或同步I鸵肌)任務(wù)鱼炒,當(dāng)你傳入一個(gè) 回調(diào)函數(shù),你就在它周圍懸掛了一些閉包蝌借!

注意: 第三章介紹了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ù)傍衡,但沒(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次氛濒,在一秒的間隔內(nèi)产场。

啊泼橘?

首先涝动,讓我們解釋一下“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è)诘谌抡故玖?code>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í)在作P燮骸)在(文件加載和)編譯期間檢查一個(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è)诖擞懻摰姆秶┕汉常谀K被導(dǎo)入時(shí)同步地加載模塊文件。

考慮這段代碼:

bar.js

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

export hello;

foo.js

// 僅僅從“bar”模塊中導(dǎo)入`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í)

閉包就像在JavaScript內(nèi)部被隔離開(kāi)的魔法世界,看起來(lá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)系作者
  • 序言:七十年代末必怜,一起剝皮案震驚了整個(gè)濱河市肉拓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌棚赔,老刑警劉巖帝簇,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件徘郭,死亡現(xiàn)場(chǎng)離奇詭異靠益,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)残揉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)胧后,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人抱环,你說(shuō)我怎么就攤上這事壳快。” “怎么了镇草?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵眶痰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我梯啤,道長(zhǎng)竖伯,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任因宇,我火速辦了婚禮七婴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘察滑。我一直安慰自己打厘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布贺辰。 她就那樣靜靜地躺著户盯,像睡著了一般嵌施。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上先舷,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天艰管,我揣著相機(jī)與錄音,去河邊找鬼蒋川。 笑死牲芋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的捺球。 我是一名探鬼主播缸浦,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼氮兵!你這毒婦竟也來(lái)了裂逐?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤泣栈,失蹤者是張志新(化名)和其女友劉穎卜高,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體南片,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡掺涛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疼进。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片薪缆。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖伞广,靈堂內(nèi)的尸體忽然破棺而出拣帽,到底是詐尸還是另有隱情,我是刑警寧澤嚼锄,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布减拭,位于F島的核電站,受9級(jí)特大地震影響区丑,放射性物質(zhì)發(fā)生泄漏拧粪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一刊苍、第九天 我趴在偏房一處隱蔽的房頂上張望既们。 院中可真熱鬧,春花似錦正什、人聲如沸啥纸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)斯棒。三九已至盾致,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荣暮,已是汗流浹背庭惜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留穗酥,地道東北人护赊。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像砾跃,于是被迫代替她去往敵國(guó)和親骏啰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容