在翻閱《你不知道的javascript》這一套書的中上卷目錄之后,發(fā)現(xiàn)書中針對(duì)閉包、對(duì)象、原型、語(yǔ)法、異步、回調(diào)等等既基礎(chǔ)又重要的
javascript知識(shí)有著針對(duì)性的闡述,于是決定對(duì)這套書的中上卷進(jìn)行學(xué)習(xí)。上卷和中卷各講述了兩大部分知識(shí),分別是:作用域與閉包、
this和對(duì)象原型、類型和語(yǔ)法、異步和性能。本文是對(duì)作用域與閉包的學(xué)習(xí)總結(jié)。
閉包,一個(gè)非常神秘的詞語(yǔ),其實(shí)不光js中存在閉包的概念,在其他編程語(yǔ)言中也有。在《你不知道的javascript(上)》一書中,通過一些易懂的實(shí)例,很容易的就解釋清楚閉包,以及它的主要作用。學(xué)習(xí)之后,我認(rèn)為對(duì)閉包主要掌握三個(gè)要點(diǎn):概念,實(shí)際用途,利用閉包來(lái)建立模塊。
1.閉包概念
正如書中所說(shuō),閉包在js中無(wú)處不在,其定義也直截了當(dāng):當(dāng)函數(shù)可以記住并訪問所在的詞法作用域時(shí),就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行。注意重點(diǎn),第一句話闡述閉包的定義,根據(jù)js引擎按照作用域鏈向上查找的機(jī)制,函數(shù)內(nèi)部嵌套的函數(shù)肯定會(huì)形成閉包,但是使一個(gè)閉包顯得有意義的卻是第二句話(當(dāng)然閉包不只這一種作用)。
如下代碼:
function foo(){
var a=2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz();//2
在foo()函數(shù)中,bar()已經(jīng)形成了一個(gè)閉包,通過return bar將bar返回給了全局作用域,而通過var聲明將bar傳遞給了baz,于是,此時(shí)baz作為在全局作用域中的標(biāo)識(shí)符,卻能夠訪問foo作用域內(nèi)的變量a,這就是閉包所起的作用。
再來(lái)看一個(gè)復(fù)雜的傳參例子:
function foo(){
var a=2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
fn();
}
在foo()函數(shù)中通過bar(baz)調(diào)用了外部函數(shù)bar(fn),也就是將baz傳給了外部函數(shù)bar(fn),既f(wàn)n=baz,因此在執(zhí)行的時(shí)候,執(zhí)行fn()也就是執(zhí)行了一次baz(),于是,作為另一個(gè)函數(shù)中的fn(),就能夠訪問到foo()中的變量a,而這是baz()閉包所起的作用。
2.閉包的作用
閉包所起的作用不只是能夠在外部作用域中訪問內(nèi)部變量,在回調(diào)函數(shù)及循環(huán)語(yǔ)句中,主要也是閉包在起作用。同樣,看一個(gè)書中提供的使用延時(shí)器的例子:
function wait(message){
setTimeout(function timer(){
console.log(message);
},1000);
}
wait("Hello,closure!")
在調(diào)用wait("Hello,closure!")之后,延時(shí)器中的函數(shù)要在1000毫秒后執(zhí)行,此時(shí),按照正常的js垃圾收回機(jī)制,wait()的作用域及其中儲(chǔ)存的變量值"Hello,closure!"應(yīng)該在wait("Hello,closure!")執(zhí)行幾微秒后就被收回了,然后正是由于延時(shí)器中的函數(shù)timer()處于wait(message)函數(shù)內(nèi)部,形成了閉包,使得timer()可以在1000毫秒后能夠訪問到變量值"Hello,closure!"??梢姡]包不僅能使外部訪問函數(shù)作用域內(nèi)的變量,還能使得函數(shù)作用域在一定時(shí)間內(nèi)保持完整。
有時(shí)候(或者說(shuō)經(jīng)常),會(huì)在for循環(huán)內(nèi)執(zhí)行延時(shí)器,如果延時(shí)器要使用for循環(huán)的i變量(j或者其他也可以,總之就是循環(huán)按照其變化執(zhí)行的那個(gè)變量),經(jīng)常會(huì)出現(xiàn)所有延時(shí)器使用的都是最后一個(gè)i值的情況出現(xiàn)。對(duì)此,需要使用閉包和IIFE才能夠很好的處理這個(gè)問題(IIFE指立即執(zhí)行函數(shù)表達(dá)式)。
如下代碼:
for (var i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000);
}
預(yù)想的情況是,每隔1000秒執(zhí)行一次timer(),并且timer()中使用的i值依次為1到5。然而實(shí)際情況卻是在6000毫秒之后同時(shí)執(zhí)行了timer(),并且timer()中使用到的i值全都是6。究其原因,需要解釋兩部分內(nèi)容:
首先是出現(xiàn)i值為6的原因,由于當(dāng)i值為5時(shí),依然滿足for循環(huán)的條件,所以循環(huán)語(yǔ)句會(huì)再執(zhí)行一次,而i++就使得最后的i值為6;其次,為何不是使用的1到5,因?yàn)閒or循環(huán)執(zhí)行完畢只是幾微秒的時(shí)間,而按照預(yù)想最早執(zhí)行的timer()也會(huì)在1000毫秒之后執(zhí)行,此時(shí)i全都變成了6,所以最后延時(shí)器中的函數(shù)全都使用的是6。
要解決這個(gè)問題,主要方法就是在延時(shí)器外邊包裹一層IIFE,在每次for循環(huán)執(zhí)行的時(shí)候,立即執(zhí)行一次IIFE并將此時(shí)的i值保存在IIFE中,以給IIFE中的延時(shí)器使用。如下代碼:
for (var i=1;i<=5;i++){
(function(){
var j=i;
setTimeout(function timer(){
console.log(j);
},j*1000);
})();
}
利用j適時(shí)保存了i值,而同時(shí)延時(shí)器是異步的(也就是說(shuō)有5個(gè)延時(shí)器在執(zhí)行),每個(gè)延時(shí)器內(nèi)部的timer()都擁有一個(gè)作用域,此時(shí)體現(xiàn)出了閉包的作用,閉包使得包裹延時(shí)器的IIFE能夠保持完整,j值得以保存,因此最后timer()使用的j變量分別是1到5。此外,利用IIFE的進(jìn)階用法,還可以省掉var j=i語(yǔ)句,如下:
for (var i=1;i<=5;i++){
(function(j){
setTimeout(function timer(){
console.log(j);
},j*1000);
})(i);
}
直接進(jìn)行了傳參。
3.模塊化
前端發(fā)展到現(xiàn)今,模塊化已經(jīng)是一個(gè)主流的概念,而如今的模塊主要都是定義一個(gè)模塊封裝函數(shù),使用戶可以自定義模塊。書中介紹了模塊封裝函數(shù)的核心概念,如下代碼:(非常重要的一段代碼,完全可以使用到自己以后的代碼中,自行建模)
var MyModules = (function Manager(){
var modules = {};
function define(name,deps,impl){
for(var i=0;i<deps.length;i++){
dep[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl,deps);
}
function get(name){
return modules[name];
}
return{
define: define,
get:get
};
})();
簡(jiǎn)單介紹一下我理解的各段代碼的作用:return()返回了MyModules的對(duì)象字面量(此處使用了閉包機(jī)制),以便外部可以使用MyModules的define和get兩個(gè)函數(shù)。define(name,deps,impl)用于自定義模塊(就是在該模塊內(nèi)自定義自己需要定義的函數(shù)),name是函數(shù)名(使用define()后成為MyModules對(duì)象的屬性),deps是自定義函數(shù)需要使用的參數(shù)組,而impl是定義的函數(shù)的具體代碼。
get(name)用于從模塊中取得自己想要使用的函數(shù)或方法。
以上,關(guān)于模塊的具體使用,在書中有實(shí)例講解了具體如何運(yùn)用,建議看書進(jìn)行了解。