作用域閉包
當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時(shí)馆蠕,就產(chǎn)生了閉包箕昭,即使函數(shù)式在當(dāng)前詞法作用域之外執(zhí)行。
下面用一些代碼來解釋這個(gè)定義:
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
這段代碼看起來和嵌套作用域中的示例代碼很相似俭令∧窍龋基于詞法作用域的查找規(guī)則,函數(shù)bar()可以訪問外部作用域中的變量a(RHS引用查詢)却音。
這是閉包嗎改抡?
技術(shù)上來講,也許是系瓢。但根據(jù)前面的定義阿纤,確切地說并不是。最準(zhǔn)確地用來解釋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)槲覀冎酪嬗欣厥掌饔脕磲尫挪辉偈褂玫膬?nèi)存空間。由于看上去foo()的內(nèi)容不會(huì)再被使用专甩,所以很自然地考慮對(duì)其進(jìn)行回收钟鸵。
而閉包的“神奇”之處正是可以阻止這件事情的發(fā)生。事實(shí)上內(nèi)部作用域依然存在涤躲,因此沒有被回收棺耍。誰在使用這個(gè)內(nèi)部作用域?原來是bar()本身在使用种樱。
拜bar()所聲明的位置所賜蒙袍,它擁有涵蓋foo()內(nèi)部作用域的閉包,使得該作用域能夠一直存活嫩挤,以供bar()在之后任何時(shí)間進(jìn)行引用害幅。
bar()依然持有對(duì)該作用域的引用,而這個(gè)引用就叫做閉包岂昭。
無論使用何種方式對(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)建的作用域封閉起來理盆。但如果作用域是空的,那么僅僅將它們進(jìn)行封閉是不夠的凑阶。仔細(xì)看一下猿规,IIFE只是一個(gè)什么都沒有的空作用域。它需要有自己的變量宙橱,用來在每個(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í)的值來初始化這個(gè)變量。
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
這就是塊作用域和閉包的結(jié)合使用洁闰。
模塊
還有其他的代碼模式利用閉包的強(qiáng)大威力歉甚,但從表面上看,它們似乎與回調(diào)無關(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ì)象卓缰,它們本身也可以擁有屬性)计呈。
如果要更簡單的描述,模塊模式需要具備兩個(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)行簡單的改進(jìn)來實(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
未來的模塊機(jī)制
ES6中為模塊增加了一級(jí)語法支持桥状。但通過模塊系統(tǒng)進(jìn)行加載時(shí),ES6會(huì)將文件當(dāng)做獨(dú)立的模塊來處理硝清。每個(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ù)需要使用任意多次侠坎。