特別說明,為便于查閱煌集,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS
希望我們是帶著對作用域工作方式的健全,堅實的理解來到這里的捌省。
我們將我們的注意力轉(zhuǎn)向這個語言中一個重要到不可思議苫纤,但是一直難以捉摸的、幾乎是神話般的 部分:閉包纲缓。如果你至此一直跟隨著我們關(guān)于詞法作用域的討論卷拘,那么你會感覺閉包將在很大程度上沒那么令人激動,幾乎是顯而易見的祝高。有一個魔法師坐在幕后栗弟,現(xiàn)在我們即將見到他。不工闺,他的名字不是 Crockford乍赫!
如果你還對詞法作用域感到不安,那么現(xiàn)在就是在繼續(xù)之前回過頭去再復(fù)習一下第二章的好時機陆蟆。
啟蒙
對于那些對 JavaScript 有些經(jīng)驗耿焊,但是也許從沒全面掌握閉包概念的人來說,理解閉包 看起來就像是必須努力并作出犧牲才能到達的涅槃狀態(tài)遍搞。
回想幾年前我對 JavaScript 有了牢固的掌握,但是不知道閉包是什么器腋。它暗示著這種語言有著另外的一面溪猿,它許諾了甚至比我已經(jīng)擁有的還多的力量,它取笑并嘲弄我纫塌。我記得我通讀早期框架的源代碼試圖搞懂它到底是如何工作的诊县。我記得第一次“模塊模式”的某些東西融入我的大腦。我記得那依然栩栩如生的 啊哈措左! 一刻依痊。
那時我不明白的東西,那個花了我好幾年時間才搞懂的東西,那個我即將傳授給你的東西胸嘁,是這個秘密:在 JavaScript 中閉包無所不在瓶摆,你只是必須認出它并接納它。閉包不是你必須學習新的語法和模式才能使用的特殊的可選的工具性宏。不群井,閉包甚至不是你必須像盧克在原力中修煉那樣,一定要學會使用并掌握的武器毫胜。
閉包是依賴于詞法作用域編寫代碼而產(chǎn)生的結(jié)果书斜。它們就這么發(fā)生了。要利用它們你甚至不需要有意地創(chuàng)建閉包酵使。閉包在你的代碼中一直在被創(chuàng)建和使用荐吉。你 缺少 的是恰當?shù)乃季S環(huán)境,來識別口渔,接納样屠,并以自己的意志利用閉包。
啟蒙的時刻應(yīng)該是:哦搓劫,閉包已經(jīng)在我的代碼中到處發(fā)生了瞧哟,現(xiàn)在我終于 看到 它們了。理解閉包就像是尼歐第一次見到母體枪向。
事實真相
好了勤揩,夸張和對電影的無恥引用夠多了。
為了理解和識別閉包秘蛔,這里有一個你需要知道的簡單粗暴的定義:
閉包就是函數(shù)能夠記住并訪問它的詞法作用域陨亡,即使當這個函數(shù)在它的詞法作用域之外執(zhí)行時。
讓我們跳進代碼來說明這個定義:
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
根據(jù)我們對嵌套作用域的討論深员,這段代碼應(yīng)當看起來很熟悉负蠕。由于詞法作用域查詢規(guī)則(在這個例子中,是一個 RHS 引用查詢)倦畅,函數(shù) bar()
可以 訪問 外圍作用域的變量 a
遮糖。
這是“閉包”嗎?
好吧叠赐,從技術(shù)上講…… 也許是欲账。但是根據(jù)我們上面的“你需要知道”的定義…… 不確切。我認為解釋 bar()
引用 a
的最準確的方式是根據(jù)詞法作用域查詢規(guī)則芭概,但是那些規(guī)則 僅僅 是閉包的(一個很重要的H弧)一部分。
從純粹的學院派角度講罢洲,上面的代碼段被認為是函數(shù) bar()
在函數(shù) foo()
的作用域上有一個 閉包(而且實際上踢故,它甚至對其他的作用域也可以訪問,比如這個例子中的全局作用域)。換一種略有不同的說法是殿较,bar()
閉住了 foo()
的作用域耸峭。為什么?因為 bar()
嵌套地出現(xiàn)在 foo()
內(nèi)部斜脂。就這么簡單抓艳。
但是,這樣一來閉包的定義就是不能直接 觀察到 的了帚戳,我們也不能看到閉包在這個代碼段中 被行使玷或。我們清楚地看到詞法作用域,但是閉包仍然像代碼后面謎一般的模糊陰影片任。
讓我們考慮這段將閉包完全帶到聚光燈下的代碼:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 -- 哇噢偏友,看到閉包了,伙計对供。
函數(shù) bar()
對于 foo()
內(nèi)的作用域擁有詞法作用域訪問權(quán)位他。但是之后,我們拿起 bar()
产场,這個函數(shù)本身鹅髓,將它像 值 一樣傳遞。在這個例子中京景,我們 return
bar
引用的函數(shù)對象本身窿冯。
在執(zhí)行 foo()
之后,我們將它返回的值(我們的內(nèi)部 bar()
函數(shù))賦予一個稱為 baz
的變量确徙,然后我們實際地調(diào)用 baz()
醒串,這將理所當然地調(diào)用我們內(nèi)部的函數(shù) bar()
,只不過是通過一個不同的標識符引用鄙皇。
bar()
被執(zhí)行了芜赌,必然的。但是在這個例子中伴逸,它是在它被聲明的詞法作用域 外部 被執(zhí)行的缠沈。
foo()
被執(zhí)行之后,一般說來我們會期望 foo()
的整個內(nèi)部作用域都將消失错蝴,因為我們知道 引擎 啟用了 垃圾回收器 在內(nèi)存不再被使用時來回收它們博烂。因為很顯然 foo()
的內(nèi)容不再被使用了,所以看起來它們很自然地應(yīng)該被認為是 消失了漱竖。
但是閉包的“魔法”不會讓這發(fā)生。內(nèi)部的作用域?qū)嶋H上 依然 “在使用”畜伐,因此將不會消失馍惹。誰在使用它?函數(shù) bar()
本身。
有賴于它被聲明的位置万矾,bar()
擁有一個詞法作用域閉包覆蓋著 foo()
的內(nèi)部作用域悼吱,閉包為了能使 bar()
在以后任意的時刻可以引用這個作用域而保持它的存在。
bar()
依然擁有對那個作用域的引用良狈,而這個引用稱為閉包后添。
所以,在幾微秒之后薪丁,當變量 baz
被調(diào)用時(調(diào)用我們最開始標記為 bar
的內(nèi)部函數(shù))遇西,它理所應(yīng)當?shù)貙帉憰r的詞法作用域擁有 訪問 權(quán),所以它可以如我們所愿地訪問變量 a
严嗜。
這個函數(shù)在它被編寫時的詞法作用域之外被調(diào)用粱檀。閉包 使這個函數(shù)可以繼續(xù)訪問它在編寫時被定義的詞法作用域。
當然漫玄,函數(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)用這個內(nèi)部函數(shù)(現(xiàn)在被標記為 fn
)皱碘,當我們這么做時,它覆蓋在 foo()
內(nèi)部作用域的閉包就可以通過 a
的訪問觀察到衡未。
這樣的函數(shù)傳遞也可以是間接的尸执。
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 將`baz`賦值給一個全局變量
}
function bar() {
fn(); // 看媽媽,我看到閉包了缓醋!
}
foo();
bar(); // 2
無論我們使用什么方法將內(nèi)部函數(shù) 傳送 到它的詞法作用域之外如失,它都將維護一個指向它最開始被聲明時的作用域的引用,而且無論我們什么時候執(zhí)行它送粱,這個閉包就會被行使褪贵。
現(xiàn)在我能看到了
前面的代碼段有些學術(shù)化,而且是人工構(gòu)建來說明 閉包的使用 的抗俄。但我保證過給你的東西不止是一個新的酷玩具脆丁。我保證過閉包是在你的現(xiàn)存代碼中無處不在的東西。現(xiàn)在讓我們 看看 真相动雹。
function wait(message) {
setTimeout( function timer(){
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
我們拿來一個內(nèi)部函數(shù)(名為 timer
)將它傳遞給 setTimeout(..)
槽卫。但是 timer
擁有覆蓋 wait(..)
的作用域的閉包,實際上保持并使用著對變量 message
的引用胰蝠。
在我們執(zhí)行 wait(..)
一千毫秒之后歼培,要不是內(nèi)部函數(shù) timer
依然擁有覆蓋著 wait()
內(nèi)部作用域的閉包震蒋,它早就會消失了。
在 引擎 的內(nèi)臟深處躲庄,內(nèi)建的工具 setTimeout(..)
擁有一些參數(shù)的引用查剖,可能稱為 fn
或者 func
或者其他諸如此類的東西。引擎 去調(diào)用這個函數(shù)噪窘,它調(diào)用我們的內(nèi)部 timer
函數(shù)笋庄,而詞法作用域依然完好無損。
閉包倔监。
或者直砂,如果你信仰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" );
我不確定你寫的是什么代碼丐枉,但我通常寫一些代碼來負責控制全球的閉包無人機軍團哆键,所以這完全是真實的!
把玩笑放在一邊瘦锹,實質(zhì)上 無論何時何地 只要你將函數(shù)作為頭等的值看待并將它們傳來傳去的話籍嘹,你就可能看到這些函數(shù)行使閉包。計時器弯院、事件處理器辱士、Ajax請求、跨窗口消息听绳、web worker颂碘、或者任何其他的異步(或同步!)任務(wù)椅挣,當你傳入一個 回調(diào)函數(shù)头岔,你就在它周圍懸掛了一些閉包!
注意: 第三章介紹了 IIFE 模式鼠证。雖然人們常說 IIFE(獨自)是一個可以觀察到閉包的例子峡竣,但是根據(jù)我們上面的定義,我有些不同意量九。
var a = 2;
(function IIFE(){
console.log( a );
})();
這段代碼“好用”适掰,但嚴格來說它不是在觀察閉包。為什么荠列?因為這個函數(shù)(就是我們這里命名為“IIFE”的那個)沒有在它的詞法作用域之外執(zhí)行类浪。它仍然在它被聲明的相同作用域中(那個同時持有 a
的外圍/全局作用域)被調(diào)用。a
是通過普通的詞法作用域查詢找到的肌似,不是通過真正的閉包费就。
雖說技術(shù)上閉包可能發(fā)生在聲明時,但它 不是 嚴格地可以觀察到的川队,因此力细,就像人們說的垦搬,它是一顆在森林中倒掉的樹,但周圍沒人去聽到它艳汽。
雖然 IIFE 本身 不是一個閉包的例子,但是它絕對創(chuàng)建了作用域对雪,而且它是我們用來創(chuàng)建可以被閉包的作用域的最常見工具之一河狐。所以 IIFE 確實與閉包有強烈的關(guān)聯(lián),即便它們本身不行使閉包瑟捣。
親愛的讀者馋艺,現(xiàn)在把這本書放下。我有一個任務(wù)給你迈套。去打開一些你最近的 JavaScript 代碼捐祠。尋找那些被你作為值的函數(shù),并識別你已經(jīng)在那里使用了閉包桑李,而你以前甚至可能不知道它踱蛀。
我會等你。
現(xiàn)在……你看到了贵白!
循環(huán) + 閉包
用來展示閉包最常見最權(quán)威的例子是老實巴交的 for 循環(huán)率拒。
for (var i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
注意: 當你將函數(shù)放在循環(huán)內(nèi)部時 Linter 經(jīng)常會抱怨,因為不理解閉包的錯誤 在開發(fā)者中太常見了禁荒。我們在這里講解如何正確地利用閉包的全部力量猬膨。但是 Linter 通常不理解這樣的微妙之處,所以它們不管怎樣都將抱怨呛伴,認為你 實際上 不知道你在做什么勃痴。
這段代碼的精神是,我們一般將 期待 它的行為是分別打印數(shù)字“1”热康,“2”沛申,……“5”,一次一個褐隆,一秒一個污它。
實際上,如果你運行這段代碼庶弃,你會得到“6”被打印5次衫贬,一秒一個。
靶ァ固惯?
首先,讓我們解釋一下“6”是從哪兒來的缴守。循環(huán)的終結(jié)條件是 i
不 <=5
葬毫。第一次滿足這個條件時 i
是6镇辉。所以,輸出的結(jié)果反映的是 i
在循環(huán)終結(jié)后的最終值贴捡。
如果多看兩眼的話這其實很明顯忽肛。超時的回調(diào)函數(shù)都將在循環(huán)的完成之后立即運行。實際上烂斋,就計時器而言屹逛,即便在每次迭代中它是 setTimeout(.., 0)
,所有這些回調(diào)函數(shù)也都仍然是嚴格地在循環(huán)之后運行的汛骂,因此每次都打印 6
罕模。
但是這里有個更深刻的問題。要是想讓它實際上如我們在語義上暗示的那樣動作帘瞭,我們的代碼缺少了什么淑掌?
缺少的東西是,我們試圖 暗示 在迭代期間蝶念,循環(huán)的每次迭代都“捕捉”一份對 i
的拷貝抛腕。但是,雖然所有這5個函數(shù)在每次循環(huán)迭代中分離地定義祸轮,由于作用域的工作方式兽埃,它們 都閉包在同一個共享的全局作用域上,而它事實上只有一個 i
适袜。
這么說來柄错,所有函數(shù)共享一個指向相同的 i
的引用是 理所當然 的。循環(huán)結(jié)構(gòu)的某些東西往往迷惑我們苦酱,使我們認為這里有其他更精巧的東西在工作售貌。但是這里沒有。這與根本沒有循環(huán)疫萤,5個超時回調(diào)僅僅一個接一個地被聲明沒有區(qū)別颂跨。
好了略水,那么替劈,回到我們火燒眉毛的問題语淘。缺少了什么崇决?我們需要更多 鈴聲 被閉包的作用域。明確地說棒搜,我們需要為循環(huán)的每次迭代都準備一個新的被閉包的作用域仆嗦。
我們在第三章中學到枝恋,IIFE 通過聲明并立即執(zhí)行一個函數(shù)來創(chuàng)建作用域每币。
讓我們試試:
for (var i=1; i<=5; i++) {
(function(){
setTimeout( function timer(){
console.log( i );
}, i*1000 );
})();
}
這好用嗎携丁?試試。我還會等你兰怠。
我來為你終結(jié)懸念梦鉴。不好用李茫。 但是為什么?很明顯我們現(xiàn)在有了更多的詞法作用域肥橙。每個超時回調(diào)函數(shù)確實閉包在每次迭代時分別被每個 IIFE 創(chuàng)建的作用域中魄宏。
擁有一個被閉包的 空的作用域 是不夠的。仔細觀察存筏。我們的 IIFE 只是一個空的什么也不做的作用域娜庇。它內(nèi)部需要 一些東西 才能變得對我們有用。
它需要它自己的變量方篮,在每次迭代時持有值 i
的一個拷貝。
for (var i=1; i<=5; i++) {
(function(){
var j = i;
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})();
}
萬歲励负!它好用了藕溅!
有些人偏好一種稍稍變形的形式:
for (var i=1; i<=5; i++) {
(function(j){
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})( i );
}
當然,因為這些 IIFE 只是函數(shù)继榆,我們可以傳入 i
巾表,如果我們樂意的話可以稱它為 j
,或者我們甚至可以再次稱它為 i
略吨。不管哪種方式集币,這段代碼都能工作。
在每次迭代內(nèi)部使用的 IIFE 為每次迭代創(chuàng)建了新的作用域翠忠,這給了我們的超時回調(diào)函數(shù)一個機會鞠苟,在每次迭代時閉包一個新的作用域,這些作用域中的每一個都擁有一個持有正確的迭代值的變量給我們訪問秽之。
問題解決了当娱!
重溫塊兒作用域
仔細觀察我們前一個解決方案的分析。我們使用了一個 IIFE 來在每一次迭代中創(chuàng)建新的作用域考榨。換句話說跨细,我們實際上每次迭代都 需要 一個 塊兒作用域。我們在第三章展示了 let
聲明河质,它劫持一個塊兒并且就在這個塊兒中聲明一個變量冀惭。
這實質(zhì)上將塊兒變成了一個我們可以閉包的作用域。所以接下來的牛逼代碼“就是好用”:
for (var i=1; i<=5; i++) {
let j = i; // 呀掀鹅,給閉包的塊兒作用域散休!
setTimeout( function timer(){
console.log( j );
}, j*1000 );
}
但是,這還不是全部淫半!(用我最棒的 Bob Barker 嗓音)在用于 for 循環(huán)頭部的 let
聲明被定義了一種特殊行為溃槐。這種行為說,這個變量將不是只為循環(huán)聲明一次科吭,而是為每次迭代聲明一次昏滴。并且猴鲫,它將在每次后續(xù)的迭代中被上一次迭代末尾的值初始化。
for (let i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
這有多酷谣殊?塊兒作用域和閉包攜手工作拂共,解決世界上所有的問題。我不知道你怎么樣姻几,但這使我成了一個快樂的 JavaScript 開發(fā)者宜狐。
模塊
還有其他的代碼模式利用了閉包的力量,但是它們都不像回調(diào)那樣浮于表面蛇捌。讓我們來檢視它們中最強大的一種:模塊抚恒。
function foo() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
}
就現(xiàn)在這段代碼來說,沒有發(fā)生明顯的閉包络拌。我們只是擁有一些私有數(shù)據(jù)變量 something
和 another
俭驮,以及幾個內(nèi)部函數(shù) doSomething()
和 doAnother()
,它們都擁有覆蓋在 foo()
內(nèi)部作用域上的詞法作用域(因此是閉包4好场)混萝。
但是現(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 中我們稱這種模式為 模塊。實現(xiàn)模塊模式的最常見方法經(jīng)常被稱為“揭示模塊”萍恕,它是我們在這里展示的方式的變種逸嘀。
讓我們檢視關(guān)于這段代碼的一些事情。
首先允粤,CoolModule()
只是一個函數(shù)崭倘,但它 必須被調(diào)用 才能成為一個被創(chuàng)建的模塊實例。沒有外部函數(shù)的執(zhí)行类垫,內(nèi)部作用域的創(chuàng)建和閉包都不會發(fā)生绳姨。
第二,CoolModule()
函數(shù)返回一個對象阔挠,通過對象字面量語法 { key: value, ... }
標記飘庄。這個我們返回的對象擁有指向我們內(nèi)部函數(shù)的引用,但是 沒有 指向我們內(nèi)部數(shù)據(jù)變量的引用购撼。我們可以將它們保持為隱藏和私有的跪削。可以很恰當?shù)卣J為這個返回值對象實質(zhì)上是一個 我們模塊的公有API迂求。
這個返回值對象最終被賦值給外部變量 foo
碾盐,然后我們可以在這個API上訪問那些屬性,比如 foo.doSomething()
揩局。
注意: 從我們的模塊中返回一個實際的對象(字面量)不是必須的毫玖。我們可以僅僅直接返回一個內(nèi)部函數(shù)。jQuery 就是一個很好地例子。jQuery
和 $
標識符是 jQuery “模塊”的公有API付枫,但是它們本身只是一個函數(shù)(這個函數(shù)本身可以有屬性烹玉,因為所有的函數(shù)都是對象)。
doSomething()
和 doAnother()
函數(shù)擁有模塊“實例”內(nèi)部作用域的閉包(通過實際調(diào)用 CoolModule()
得到的)阐滩。當我們通過返回值對象的屬性引用二打,將這些函數(shù)傳送到詞法作用域外部時,我們就建立好了可以觀察和行使閉包的條件掂榔。
更簡單地說继效,行使模塊模式有兩個“必要條件”:
必須有一個外部的外圍函數(shù),而且它必須至少被調(diào)用一次(每次創(chuàng)建一個新的模塊實例)装获。
外圍的函數(shù)必須至少返回一個內(nèi)部函數(shù)瑞信,這樣這個內(nèi)部函數(shù)才擁有私有作用域的閉包,并且可以訪問和/或修改這個私有狀態(tài)穴豫。
一個僅帶有一個函數(shù)屬性的對象不是 真正 的模塊喧伞。從可觀察的角度來說,一個從函數(shù)調(diào)用中返回的對象绩郎,僅帶有數(shù)據(jù)屬性而沒有閉包的函數(shù),也不是 真正 的模塊翁逞。
上面的代碼段展示了一個稱為 CoolModule()
獨立的模塊創(chuàng)建器肋杖,它可以被調(diào)用任意多次,每次創(chuàng)建一個新的模塊實例挖函。這種模式的一個稍稍的變化是當你只想要一個實例的時候状植,某種“單例”:
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
這里,我們將模塊放進一個 IIFE(見第三章)中怨喘,而且我們 立即 調(diào)用它津畸,并把它的返回值直接賦值給我們單獨的模塊實例標識符 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"
另一種在模塊模式上微小但是強大的變化是肉拓,為你作為公有API返回的對象命名:
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
通過在模塊實例內(nèi)部持有一個指向公有API對象的內(nèi)部引用,你可以 從內(nèi)部 修改這個模塊梳庆,包括添加和刪除方法暖途,屬性,和 改變它們的值膏执。
現(xiàn)代的模塊
各種模塊依賴加載器/消息機制實質(zhì)上都是將這種模塊定義包裝進一個友好的API驻售。與其檢視任意一個特定的庫,不如讓我 (僅)為了說明的目的 展示一個 非常簡單 的概念證明:
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)
更米。這為一個模塊調(diào)用了它的定義的包裝函數(shù)(傳入所有依賴)欺栗,并將返回值,也就是模塊的API,存儲到一個用名稱追蹤的內(nèi)部模塊列表中迟几。
這里是我可能如何使用它來定義一個模塊:
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”都使用一個返回公有API的函數(shù)來定義消请。“foo”甚至接收一個“bar”的實例作為依賴參數(shù)瘤旨,并且可以因此使用它梯啤。
花些時間檢視這些代碼段,來完全理解將閉包的力量付諸實踐給我們帶來的好處存哲。關(guān)鍵之處在于因宇,對于模塊管理器來說真的沒有什么特殊的“魔法”。它們只是滿足了我在上面列出的模塊模式的兩個性質(zhì):調(diào)用一個函數(shù)定義包裝器祟偷,并將它的返回值作為這個模塊的API保存下來察滑。
換句話說,模塊就是模塊修肠,即便你在它們上面放了一個友好的包裝工具贺辰。
未來的模塊
ES6 為模塊的概念增加了頭等的語法支持。當通過模塊系統(tǒng)加載時嵌施,ES6 將一個文件視為一個獨立的模塊饲化。每個模塊可以導(dǎo)入其他的模塊或者特定的API成員,也可以導(dǎo)出它們自己的公有API成員吗伤。
注意: 基于函數(shù)的模塊不是一個可以被靜態(tài)識別的模式(編譯器可以知道的東西)吃靠,所以它們的API語義直到運行時才會被考慮。也就是足淆,你實際上可以在運行時期間修改模塊的API(參見早先 publicAPI
的討論)巢块。
相比之下,ES6 模塊API是靜態(tài)的(這些API不會在運行時改變)巧号。因為編譯器知道它族奢,它可以(也確實在這么作!)在(文件加載和)編譯期間檢查一個指向被導(dǎo)入模塊的成員的引用是否 實際存在丹鸿。如果API引用不存在越走,編譯器就會在編譯時拋出一個“早期”錯誤,而不是等待傳統(tǒng)的動態(tài)運行時解決方案(和錯誤靠欢,如果有的話)弥姻。
ES6 模塊 沒有 “內(nèi)聯(lián)”格式,它們必須被定義在一個分離的文件中(每個模塊一個)掺涛。瀏覽器/引擎擁有一個默認的“模塊加載器”(它是可以被覆蓋的庭敦,但是這超出我們在此討論的范圍),它在模塊被導(dǎo)入時同步地加載模塊文件薪缆。
考慮這段代碼:
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`整個模塊
module foo from "foo";
module bar from "bar";
console.log(
bar.hello( "rhino" )
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO
注意: 需要使用前兩個代碼片段中的內(nèi)容分別創(chuàng)建兩個分離的文件 “foo.js” 和 “bar.js”秧廉。然后伞广,你的程序?qū)⒓虞d/導(dǎo)入這些模塊來使用它們,就像第三個片段那樣疼电。
import
在當前的作用域中導(dǎo)入一個模塊的API的一個或多個成員嚼锄,每個都綁定到一個變量(這個例子中是 hello
)。module
將整個模塊的API導(dǎo)入到一個被綁定的變量(這個例子中是 foo
蔽豺,bar
)区丑。export
為當前模塊的公有API導(dǎo)出一個標識符(變量,函數(shù))修陡。在一個模塊的定義中沧侥,這些操作符可以根據(jù)需要使用任意多次。
在 模塊文件 內(nèi)部的內(nèi)容被視為像是包圍在一個作用域閉包中魄鸦,就像早先看到的使用函數(shù)閉包的模塊那樣宴杀。
復(fù)習
對于那些還蒙在鼓里的人來說,閉包就像在 JavaScript 內(nèi)部被隔離開的魔法世界拾因,只有很少一些最勇敢的靈魂才能到達旺罢。但是它實際上只是一個標準的,而且?guī)缀趺黠@的事實 —— 我們?nèi)绾卧诤瘮?shù)即是值绢记,而且可以被隨意傳遞的詞法作用域環(huán)境中編寫代碼扁达,
閉包就是當一個函數(shù)即使是在它的詞法作用域之外被調(diào)用時,也可以記住并訪問它的詞法作用域蠢熄。
如果我們不能小心地識別它們和它們的工作方式跪解,閉包可能會絆住我們,例如在循環(huán)中护赊。但它們也是一種極其強大的工具,以各種形式開啟了像 模塊 這樣的模式砾跃。
模塊要求兩個關(guān)鍵性質(zhì):1)一個被調(diào)用的外部包裝函數(shù)骏啰,來創(chuàng)建外圍作用域。2)這個包裝函數(shù)的返回值必須包含至少一個內(nèi)部函數(shù)的引用抽高,這個函數(shù)才擁有包裝函數(shù)內(nèi)部作用域的閉包判耕。
現(xiàn)在我們看到了閉包在我們的代碼中無處不在,而且我們有能力識別它們翘骂,并為了我們自己的利益利用它們壁熄!