作用域閉包
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時(shí),就產(chǎn)生了閉包,即使函數(shù)式在當(dāng)前詞法作用域之外執(zhí)行。
下面用一些代碼來(lái)解釋這個(gè)定義:
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
這段代碼看起來(lái)和嵌套作用域中的示例代碼很相似?;谠~法作用域的查找規(guī)則,函數(shù)bar()可以訪問外部作用域中的變量a(RHS引用查詢)。
這是閉包嗎?
技術(shù)上來(lái)講,也許是。但根據(jù)前面的定義,確切地說并不是。最準(zhǔn)確地用來(lái)解釋bar()對(duì)a的引用的方法是詞法作用域的查找規(guī)則,而這些規(guī)則只是閉包的一部分。
下面代碼清晰地展示了閉包:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,這就是閉包的效果。
以上代碼,函數(shù)bar()的詞法作用域能夠訪問foo()的內(nèi)部作用域。然后我們將bar()函數(shù)本身當(dāng)做一個(gè)類型進(jìn)行傳遞。在這個(gè)例子中,我們將bar所引用的函數(shù)對(duì)象本身當(dāng)做返回值。
在foo()執(zhí)行后,其返回值(也就是內(nèi)部的bar()函數(shù))賦值給變量baz并調(diào)用baz(),實(shí)際上只是通過不同的標(biāo)識(shí)符引用調(diào)用了內(nèi)部的函數(shù)bar()。
bar()顯然可以被正常執(zhí)行。但是在這個(gè)例子中,它在自己定義的詞法作用域以外的地方執(zhí)行。
在foo()執(zhí)行后,通常會(huì)期待foo()的整個(gè)內(nèi)部作用域都被銷毀,因?yàn)槲覀冎酪嬗欣厥掌饔脕?lái)釋放不再使用的內(nèi)存空間。由于看上去foo()的內(nèi)容不會(huì)再被使用,所以很自然地考慮對(duì)其進(jìn)行回收。
而閉包的“神奇”之處正是可以阻止這件事情的發(fā)生。事實(shí)上內(nèi)部作用域依然存在,因此沒有被回收。誰(shuí)在使用這個(gè)內(nèi)部作用域?原來(lái)是bar()本身在使用。
拜bar()所聲明的位置所賜,它擁有涵蓋foo()內(nèi)部作用域的閉包,使得該作用域能夠一直存活,以供bar()在之后任何時(shí)間進(jìn)行引用。
bar()依然持有對(duì)該作用域的引用,而這個(gè)引用就叫做閉包。
無(wú)論使用何種方式對(duì)函數(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(); // 媽媽快看呀,這就是閉包!
}
把內(nèi)部函數(shù)baz傳遞給bar,當(dāng)調(diào)用這個(gè)內(nèi)部函數(shù)時(shí)(現(xiàn)在叫fn),它涵蓋的foo()內(nèi)部作用域的閉包就可以觀察到了,因?yàn)樗軌蛟L問a。
傳遞函數(shù)也可以是間接的:
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 將baz 分配給全局變量
}
function bar() {
fn(); // 媽媽快看呀,這就是閉包!
}
foo();
bar(); // 2
循環(huán)和閉包
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
正常情況下,我們對(duì)這段代碼行為的預(yù)期是分別輸出數(shù)字1-5,每秒一次,每次一個(gè)。
但實(shí)際上,這段代碼在運(yùn)行時(shí)會(huì)以每秒一次的頻率輸出五次6。
我們?cè)噲D假設(shè)循環(huán)中的每個(gè)迭代在運(yùn)行時(shí)都會(huì)給自己“捕獲”一個(gè)i的副本。但是根據(jù)作用域的工作原理,實(shí)際情況是盡管循環(huán)中的五個(gè)函數(shù)式在各個(gè)迭代中分別定義的,但是它們都被封閉在一個(gè)共享的全局作用域中,因此實(shí)際上只有一個(gè)i。
這樣說的話,當(dāng)然所有函數(shù)共享一個(gè)i的引用。循環(huán)結(jié)構(gòu)讓我們誤以為背后還有更復(fù)雜的機(jī)制在起作用,但實(shí)際上沒有。如果將延遲函數(shù)的回調(diào)重復(fù)定義五次,完全不使用循環(huán),那它同這段代碼是完全等價(jià)的。
以上代碼的缺陷是什么?我們需要更多的閉包作用域,特別是在循環(huán)的過程中每個(gè)迭代都需要一個(gè)閉包作用域。
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
如果改成以上代碼,也是不行的。的確我們現(xiàn)在擁有了更多的詞法作用域,每個(gè)延遲函數(shù)都會(huì)將IIFE在每次迭代中創(chuàng)建的作用域封閉起來(lái)。但如果作用域是空的,那么僅僅將它們進(jìn)行封閉是不夠的。仔細(xì)看一下,IIFE只是一個(gè)什么都沒有的空作用域。它需要有自己的變量,用來(lái)在每個(gè)迭代中存儲(chǔ)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 );
}
重返塊作用域
仔細(xì)思考我們對(duì)前面的解決方案的分析。我們使用IIFE在每次迭代時(shí)都創(chuàng)建一個(gè)新的作用域。換句話說,每次迭代我們都需要一個(gè)塊作用域。
for (var i=1; i<=5; i++) {
let j = i; // 是的,閉包的塊作用域!
setTimeout( function timer() {
console.log( j );
}, j*1000 );
}
但是以上代碼還不全部。for循環(huán)頭部的let聲明還會(huì)有一個(gè)特殊的行為。這個(gè)行為指出變量在循環(huán)過程中不止被聲明一次,每次迭代都會(huì)聲明。隨后的每個(gè)迭代都會(huì)使用上一個(gè)迭代結(jié)束時(shí)的值來(lái)初始化這個(gè)變量。
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
這就是塊作用域和閉包的結(jié)合使用。
模塊
還有其他的代碼模式利用閉包的強(qiáng)大威力,但從表面上看,它們似乎與回調(diào)無(wú)關(guā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
這個(gè)模式在JavaScript中被稱為模塊。最常見的實(shí)現(xiàn)模塊模式的方法通常被稱為模塊暴露,這里展示的是其變體。
從模塊中返回一個(gè)實(shí)際的對(duì)象并不是必須的,也可以直接返回一個(gè)內(nèi)部函數(shù)。JQuery就是一個(gè)很好的例子。JQuery和$標(biāo)識(shí)符就是JQuery模塊的公共API,但它們本身都是函數(shù)(由于函數(shù)也是對(duì)象,它們本身也可以擁有屬性)。
如果要更簡(jiǎn)單的描述,模塊模式需要具備兩個(gè)必要條件:
1、必須有外部的封裝函數(shù),該函數(shù)必須至少被調(diào)用一次(每次調(diào)用都會(huì)創(chuàng)建一個(gè)新的模塊實(shí)例)。
2、封裝函數(shù)必須返回至少一個(gè)內(nèi)部函數(shù),這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態(tài)。
一個(gè)具有函數(shù)屬性的對(duì)象本身并不是真正的模塊。從方便觀察的角度看,一個(gè)從函數(shù)調(diào)用所返回的,只有數(shù)據(jù)屬性而沒有閉包函數(shù)的對(duì)象并不是真正的模塊。
上一個(gè)示例代碼中有一個(gè)叫做CoolModule()的獨(dú)立的模塊創(chuàng)建器,可以被調(diào)用任意多次,每次調(diào)用都會(huì)創(chuàng)建一個(gè)新的模塊實(shí)例。當(dāng)只需要一個(gè)實(shí)例時(shí),可以對(duì)這個(gè)模式進(jìn)行簡(jiǎn)單的改進(jìn)來(lái)實(shí)現(xiàn)單例模式:
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
現(xiàn)代的模塊機(jī)制
大多數(shù)模塊依賴加載器/管理器本質(zhì)上都是將這種模塊定義封裝進(jìn)一個(gè)友好的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
};
})();
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
未來(lái)的模塊機(jī)制
ES6中為模塊增加了一級(jí)語(yǔ)法支持。但通過模塊系統(tǒng)進(jìn)行加載時(shí),ES6會(huì)將文件當(dāng)做獨(dú)立的模塊來(lái)處理。每個(gè)模塊都可以導(dǎo)入其他模塊或特定的API成員,同樣也可以導(dǎo)出自己的API成員。
// 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;
baz.js
// 導(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
import可以將一個(gè)模塊中的一個(gè)或多個(gè)API導(dǎo)入到當(dāng)前作用域中,并分別綁定在一個(gè)變量上(在我們的例子里是hello)。module會(huì)將整個(gè)模塊的API導(dǎo)入并綁定到一個(gè)變量上(在我們的例子里是foo和bar)。export會(huì)將當(dāng)前模塊的一個(gè)標(biāo)識(shí)符(變量、函數(shù))導(dǎo)出為公共API。這些操作可以在模塊定義中根據(jù)需要使用任意多次。
