當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時(shí),就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用 域之外執(zhí)行谨垃。
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
這是閉包嗎?
技術(shù)上來講,也許是距芬。但根據(jù)前面的定義,確切地說并不是解滓。因?yàn)椴]有體現(xiàn)出bar記住了所在的詞法作用域踊谋。我認(rèn)為最準(zhǔn)確地用來解釋 bar() 對 a 的引用的方法是詞法作用域的查找規(guī)則,而這些規(guī)則只是閉包的一部分。(但卻 是非常重要的一部分!)
下面我們來看一段代碼,清晰地展示了閉包:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,這就是閉包的效果后裸。
拜 bar() 所聲明的位置所賜,它擁有涵蓋 foo() 內(nèi)部作用域的閉包,使得該作用域能夠一 直存活,以供 bar() 在之后任何時(shí)間進(jìn)行引用辙诞。
bar() 依然持有對該作用域的引用,而這個(gè)引用就叫作閉包。
因此,在幾微秒之后變量 baz 被實(shí)際調(diào)用(調(diào)用內(nèi)部函數(shù) bar),不出意料它可以訪問定義時(shí)的詞法作用域,因此它也可以如預(yù)期般訪問變量 a轻抱。
當(dāng)然,無論使用何種方式對函數(shù)類型的值進(jìn)行傳遞,當(dāng)函數(shù)在別處被調(diào)用時(shí)都可以觀察到 閉包。
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 媽媽快看呀,這就是閉包! bar函數(shù)居然訪問到了foo函數(shù)的作用域旦部。
}
把內(nèi)部函數(shù) baz 傳遞給 bar,當(dāng)調(diào)用這個(gè)內(nèi)部函數(shù)時(shí)(現(xiàn)在叫作 fn),它涵蓋的 foo() 內(nèi)部
作用域的閉包就可以觀察到了,因?yàn)樗軌蛟L問 a了祈搜。等于在bar的作用域中讀取到foo的作用域,所以產(chǎn)生了閉包士八。
來看看平常寫代碼遇到的閉包
function wait(msg) {
setTimeout(function timer(){
console.log(msg)
},1000)
}
wait("hello")
這怎么就是閉包了呢容燕?
沒關(guān)系,如果這個(gè)看不出來的話婚度,我們不妨把代碼變化一下
function wait(msg) {
function timer(){
console.log(msg)
}
setTimeout(timer,1000)
}
wait("hello")
如果還是理解不了的話蘸秘。我們再來舉出一個(gè)例子
function wait(msg) {
function timer(){
console.log(msg)
}
fn(timer)
}
function fn(fnc){
fnc();
}
wait("hello")
這就能看出來是如何使用的閉包了吧。
將一個(gè)內(nèi)部函數(shù)(名為 timer)傳遞給 setTimeout(..)蝗茁。timer 具有涵蓋 wait(..) 作用域的閉包,因此還保有對變量 msg 的引用醋虏。
類似的還有
function setupBot(name, selector) {
$( selector ).click( function activator() {
console.log( "Activating: " + name );
} );
}
setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );
使得 $( selector ).click 函數(shù)內(nèi)部可以讀取setupBot函數(shù)的作用域。
傳遞函數(shù)當(dāng)然也可以是間接的哮翘。
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í)行這個(gè)函數(shù)都會使用閉包颈嚼。本質(zhì)上無論何時(shí)何地,如果將函數(shù)(訪問它們各自的詞法作用域)當(dāng)作第一 級的值類型并到處傳遞,你就會看到閉包在這些函數(shù)中的應(yīng)用。在定時(shí)器饭寺、事件監(jiān)聽器阻课、 Ajax請求叫挟、跨窗口通信、Web Workers或者任何其他的異步(或者同步)任務(wù)中,只要使 用了回調(diào)函數(shù),實(shí)際上就是在使用閉包!
循環(huán)和閉包
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
為什么打印5個(gè)6
仔細(xì)想一下,這好像又是顯而易見的,延遲函數(shù)的回調(diào)會在循環(huán)結(jié)束時(shí)才執(zhí)行限煞。事實(shí)上, 當(dāng)定時(shí)器運(yùn)行時(shí)即使每個(gè)迭代中執(zhí)行的是setTimeout(.., 0),所有的回調(diào)函數(shù)依然是在循 環(huán)結(jié)束后才會被執(zhí)行,因此會每次輸出一個(gè) 6 出來抹恳。
這里引伸出一個(gè)更深入的問題,代碼中到底有什么缺陷導(dǎo)致它的行為同語義所暗示的不一 致呢?
缺陷是我們試圖假設(shè)循環(huán)中的每個(gè)迭代在運(yùn)行時(shí)都會給自己“捕獲”一個(gè) i 的副本。但是 根據(jù)作用域的工作原理,實(shí)際情況是盡管循環(huán)中的五個(gè)函數(shù)是在各個(gè)迭代中分別定義的, 但是它們都被封閉在一個(gè)共享的全局作用域中,因此實(shí)際上只有一個(gè) i署驻。
通過聲明并立即執(zhí)行一個(gè)函數(shù)來創(chuàng)建作用域奋献。
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
構(gòu)造函數(shù)和閉包
構(gòu)造函數(shù)可以通過new 來調(diào)用產(chǎn)生對象。每次new都會產(chǎn)生一個(gè)新的作用域硕舆。這個(gè)作用域在哪呢秽荞?我們先看看new的過程是什么
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj)
JS中一般產(chǎn)生作用域的為函數(shù)塊(當(dāng)然let,catch塊也可以產(chǎn)生作用域)抚官。所以每一個(gè)Base.call(obj) 都會產(chǎn)生一個(gè)作用域扬跋。如果不存在閉包的話,這個(gè)作用域很快就會被釋放凌节。那怎么產(chǎn)生閉包呢钦听?
function MyObject(){
//私有變量或私有函數(shù)(使用var 定義的變量)
var privateVariable=10;
// 特權(quán)方法 (掛在this上的變量)
this.publicMethod=function(){
privateVariable++;
};
}
var obj1 = new MyObject();
var obj2 = new MyObject();
obj1 和 obj2都可以通過publicMethod去修改privateVariable變量。但是這兩個(gè)變量之間沒有聯(lián)系倍奢,是單獨(dú)存在的朴上。因?yàn)閚ew的操作使obj1對象和obj2對象都有了個(gè)屬性publicMethod可以訪問到privateVariable變量,就使每次的MyObject作用域不能釋放卒煞,造成了閉包痪宰。
在構(gòu)造函數(shù)內(nèi)部定義了所有私有變量和函數(shù),又繼續(xù)創(chuàng)建了能夠訪問這些私有成員的特權(quán)方法畔裕。能在構(gòu)造函數(shù)中定義特權(quán)方法是因?yàn)樘貦?quán)方法作為閉包有權(quán)訪問在構(gòu)造函數(shù)中定義的所有變量和函數(shù)衣撬。
構(gòu)造函數(shù)的閉包