閉包是js中一個(gè)極為NB的武器,但也不折不扣的成了初學(xué)者的難點(diǎn)躲履。因?yàn)閷W(xué)好閉包就要學(xué)好作用域见间,正確理解作用域鏈,然而想做到這一點(diǎn)就要深入的理解函數(shù)工猜,所以我們從函數(shù)說起米诉。
函數(shù)的聲明和調(diào)用
首先說明一下,本文基于原生js環(huán)境篷帅,不涉及DOM部分
最基本的就是函數(shù)的定義和調(diào)用史侣,注意區(qū)分以下形式:
//以2下個(gè)是函數(shù)的定義
function func(){ //函數(shù)聲明
/*code*/
}
var func = function(){ //函數(shù)表達(dá)式
/*code*/
};
//以下2個(gè)是函數(shù)的調(diào)用(執(zhí)行)
func(); //無法得到函數(shù)的返回值
var returnValue = func(); //執(zhí)行函數(shù)并將返回值賦給returnValue, 如果函數(shù)沒有指定返回值,返回undefined
//以下2各定義了立即執(zhí)行函數(shù)
(function(){
/*code*/
})();
(function(){
/*code*/
}());
立即執(zhí)行函數(shù)直接聲明一個(gè)匿名函數(shù)魏身,立即使用惊橱,省得定義一個(gè)用一次就不用的函數(shù),而且免了命名沖突的問題叠骑。如果寫為如下形式可獲得立即執(zhí)行函數(shù)的返回值李皇。
var returnValue = (function(){return 1;}());
var returnValue = (function(){return 1;})();
除此之外削茁,函數(shù)還有一種非常常見的調(diào)用方式——回調(diào)函數(shù)宙枷。將一個(gè)函數(shù)作為參數(shù)傳入另一個(gè)函數(shù)掉房,并在這個(gè)函數(shù)內(nèi)執(zhí)行。比如下面這個(gè)形式
document.addEventListener("click", console.log, false);
理解了上面的部分慰丛,我們看一個(gè)典型的例子卓囚,好好理解一下函數(shù)的定義和調(diào)用的關(guān)系,這個(gè)一定要分清诅病。下面這段代碼很具有代表性:
var arr = [];
for(var i = 0; i < 10; i++){
arr[i] = function(){
return i;
};
}
for(var j = 0; j < arr.length; j++){
console.log(arr[j]() + " ");
} //得到輸出:10 10 10 10 10 10 10 10 10 10
我們需要理解這里面第一個(gè)for循環(huán)其實(shí)相當(dāng)于如下形式哪亿,它只是定義了10個(gè)函數(shù),并把函數(shù)放在數(shù)組中,并沒有執(zhí)行函數(shù)贤笆。由于js遵循詞法作用域(lexical scoping), i是一個(gè)全局變量蝇棉,所以第二個(gè)for循環(huán)調(diào)用函數(shù)的時(shí)候,i等于10
var i = 0;
arr[0] = function(){ return i; }; i++;
arr[1] = function(){ return i; }; i++;
arr[2] = function(){ return i; }; i++;
//......省略
arr[9] = function(){ return i; }; i++;
//此時(shí)i == 10 循環(huán)結(jié)束
再講完了閉包我們?cè)倩貋斫鉀Q這個(gè)問題芥永。
關(guān)于函數(shù)的參數(shù)傳遞這里就不多說了篡殷,值得強(qiáng)調(diào)的是,上述2種定義函數(shù)的方式是有區(qū)別的埋涧,想理解這個(gè)區(qū)別板辽,先要理解聲明提前。
變量聲明提前
這個(gè)地方簡單理解一下js的預(yù)處理過程棘催。js代碼會(huì)在執(zhí)行前進(jìn)行預(yù)處理劲弦,預(yù)處理的時(shí)候會(huì)進(jìn)行變量聲明提前,每個(gè)作用域的變量(用var聲明的變量醇坝,沒有用var聲明的變量不會(huì)提前)和函數(shù)定義會(huì)提前到這個(gè)作用域內(nèi)的開頭邑跪。
函數(shù)中的變量聲明會(huì)提前到函數(shù)的開始,但初始化不會(huì)呼猪。比如下面這個(gè)代碼呀袱。因此我們應(yīng)該避免在函數(shù)中間聲明變量,以增加代嗎的可讀性郑叠。
function(){
console.log(a); //undefined
f(); //f called
/*...*/
function f(){
console.log("f called");
}
var a = 3;
console.log(a); //3
}
這段代碼等于(并且瀏覽器也是這么做的):
function(){
function f(){
console.log("f called");
}
var a;
console.log(a); //undefined
f(); //f called
/*...*/
a = 3;
console.log(a); //3
}
不同函數(shù)定義方式的區(qū)別
第一個(gè)區(qū)別:
function big(){
func();//函數(shù)正常執(zhí)行
func1();//TypeError: func1 is not a function
function func(){ //這個(gè)函數(shù)聲明會(huì)被提前
console.log("func is called");
}
var func1 = function(){ //這個(gè)函數(shù)聲明會(huì)被提前夜赵,但不是個(gè)函數(shù),而是變量
console.log("func1 is called");
};
}
big();
第二個(gè)區(qū)別乡革,比較下面2段代碼
function f() {
var b=function(){return 1;};
function b(){return 0;};
console.log(b());
console.log(a());
function a(){return 0;};
var a=function(){return 1;};
}
f();
不難發(fā)現(xiàn)寇僧,用表達(dá)式定義的函數(shù)可以覆蓋函數(shù)聲明直接定義的函數(shù);但是函數(shù)聲明定義的函數(shù)卻不能覆蓋表達(dá)式定義的函數(shù)沸版。
實(shí)際中我們發(fā)現(xiàn)嘁傀,定義在調(diào)用之前var f = function(){};
會(huì)覆蓋function f(){}
,而定義在調(diào)用之后function f(){}
會(huì)覆蓋var f = function(){};
(你可以以不同順序組合交換上面代碼中的行,驗(yàn)證這個(gè)結(jié)論)
第三個(gè)區(qū)別视粮,其實(shí)這個(gè)算不上區(qū)別
var fun = function fun1(){
//內(nèi)部可見:fun和fun1
console.log(fun1 === fun);
};
//外部僅fun可見
fun(); //true 說明這是同一個(gè)對(duì)象的2各不同引用
fun1(); //ReferenceError: fun1 is not defined
此外還有一個(gè)定義方法如下:
var func = new Function("alert('hello')");
這個(gè)方式不常用细办,也不建議使用。因?yàn)樗x的函數(shù)都是在window中的,更嚴(yán)重的是笑撞,這里的代碼實(shí)在eval()
中解析的岛啸,這使得這個(gè)方式很糟糕,會(huì)帶來性能下降和安全風(fēng)險(xiǎn)茴肥。具體就不贅述了坚踩。
詞法作用域
C++和Java等語言使用的都是塊級(jí)作用域,js與它們不同瓤狐,遵循詞法作用域(lexical scoping)瞬铸。講的通俗一些,就是函數(shù)定義決定變量的作用域础锐,函數(shù)內(nèi)是一部分嗓节,函數(shù)外是另一部分,內(nèi)部可以訪問外部的變量皆警,但外部無法直接訪問內(nèi)部的變量赦政。首先我們看下面這個(gè)代碼
//這里是全局作用域
var a = 3;
var b = 2;
var c = 20;
function f(){ //這里是一個(gè)局部作用域
var a = 12; //這是一個(gè)局部變量
b = 10; //覆蓋了全局變量
var d = e = 15; //只有第一參數(shù)d是局部變量,后面的都是全局變量
f = 13; //新的全局變量
console.log(a + " " + b + " " + d);
}
f(); //12 10 15
console.log(a); //3
console.log(b); //10
console.log(c); //20
console.log(d); //undefined
console.log(e); //15
console.log(f); //13
<small>注:原生js在沒有定使用義的變量時(shí)會(huì)得到undefined耀怜,并在使用過程中遵循隱式類型轉(zhuǎn)換恢着,但現(xiàn)在的瀏覽器不會(huì)這樣,它們會(huì)直接報(bào)錯(cuò)财破。不過在函數(shù)中使用滯后定義的變量依然是undefined掰派,不會(huì)報(bào)錯(cuò),這里遵循聲明提前的原則左痢。</small>
這是一個(gè)最基本的作用域模型靡羡。我們上文提到過,函數(shù)里面可以訪問外面的變量俊性,函數(shù)外部不能直接訪問內(nèi)部的變量.
我們?cè)倏匆粋€(gè)復(fù)雜一點(diǎn)的:
var g = "g";
function f1(a){
var b = "f1";
function f2(){
var c = "f2";
console.log(a + b + c + g);
}
f2();
}
f1("g"); //gf1f2g
在js中略步,函數(shù)里面定義函數(shù)十分普遍,這就需要我們十分了解作用域鏈定页。
如下這個(gè)代碼定義了下圖中的作用域鏈:
var g = 10;
function f1(){
var f_1 = "f1";
function f2(){
var f_2 = "f2";
function f3(){
var f_3 = "f3";
/*function f...*/
}
}
}
這里內(nèi)層的函數(shù)可以由內(nèi)向外查找外層的變量(或函數(shù))趟薄,當(dāng)找到相應(yīng)的變量(或函數(shù))立即停止向外查找,并使用改變量(或函數(shù))典徊。而外層的函數(shù)不能訪問內(nèi)層的變量(或函數(shù))杭煎,這樣的層層嵌套就形成了作用域鏈。
值得一提的是卒落,函數(shù)的參數(shù)在作用于上相當(dāng)于在函數(shù)內(nèi)部第一行就聲明了的變量羡铲,注意這里指的僅僅是聲明,但不一定完成初始化儡毕,也就說明參數(shù)在沒有傳入值的時(shí)候值為undefined也切。
回調(diào)函數(shù)
那么問題來了,在一個(gè)函數(shù)外部永遠(yuǎn)不能訪問函數(shù)內(nèi)部的變量嗎?答案是否定的雷恃,我們可以用回調(diào)函數(shù)實(shí)現(xiàn)這個(gè)過程:
function A(arg){
console.log(arg);
}
function B(fun){
var a = "i am in function B";
var i = 10;
fun(a);
}
B(A); //i am in function B
上面這個(gè)過程對(duì)于B而言疆股,只把自己內(nèi)部的變量a給了fun,而外部的A無論如何也訪問不到B中的i變量褂萧,也就是說傳入的fun函數(shù)只能訪問B想讓它訪問的變量,因此回調(diào)函數(shù)這樣的設(shè)計(jì)可以在代碼的隔離和開放中間取得一個(gè)極好的平衡葵萎。
說句題外話:javascript特別適用于事件驅(qū)動(dòng)編程导犹,因?yàn)榛卣{(diào)模式支持程序以異步方式運(yùn)行。
好了羡忘,如果上面的你都看懂了谎痢,那么可以開始看閉包了。
閉包
閉包是指有權(quán)訪問另一個(gè)函數(shù)作用域中的變量的函數(shù)卷雕,創(chuàng)建閉包的最常見的方式就是在一個(gè)函數(shù)內(nèi)創(chuàng)建另一個(gè)函數(shù)节猿,通過另一個(gè)函數(shù)訪問這個(gè)函數(shù)的局部變量。閉包主要是為了區(qū)分私有和公有的方法和變量漫雕,類似于c++和java中對(duì)象的public成員和protected成員滨嘱。
一言以蔽之:作用域的嵌套構(gòu)成閉包!
構(gòu)成閉包以下幾個(gè)必要條件
- 函數(shù)(作用域)嵌套函數(shù)
- 函數(shù)(作用域)內(nèi)部可以引用外部的參數(shù)和變量
- 參數(shù)和變量不會(huì)被垃圾回收機(jī)制回收浸间√辏可以查看: 內(nèi)存管理與垃圾回收
閉包的優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn)
- 希望一個(gè)變量長期駐扎在內(nèi)存中(如同c++中static局部變量)
- 避免全局變量的污染
- 私有成員的存在
- 缺點(diǎn)
- 閉包常駐內(nèi)存,會(huì)增大內(nèi)存使用量魁蒜,大量使用影響程序性能囊扳。
- 使用不當(dāng)很容易造成內(nèi)存泄露《悼矗可以查看: 內(nèi)存管理與垃圾回收锥咸。
一般函數(shù)執(zhí)行完畢后,局部活動(dòng)對(duì)象就被銷毀细移,內(nèi)存中僅僅保存全局作用域搏予。但閉包不會(huì)!
為什么有閉包
我們考慮實(shí)現(xiàn)一個(gè)局部變量調(diào)用并自加的過程:
var a = 0;
function fun(){
return a++;
}
fun(); //返回0
fun(); //返回1
fun(); //返回2
function func(){
var a = 0;
return a++;
}
func(); //返回0
func(); //返回0
func(); //返回0
看了上面代碼你會(huì)發(fā)現(xiàn)弧轧,當(dāng)a是全局變量的時(shí)候可以實(shí)現(xiàn)缔刹,但a成為了局部變量就不行了,當(dāng)然劣针,必須是閉包才可以實(shí)現(xiàn)這個(gè)功能:
var f = (function(){
var a = 0;
return function(){
return a++;
}
})();
f(); //返回0
f(); //返回1
f(); //返回2
這樣不僅實(shí)現(xiàn)了功能校镐,還防止了可能的全局污染。
上文舉了在循環(huán)內(nèi)定義函數(shù)訪問循環(huán)變量的例子捺典,可結(jié)果并不如意鸟廓,得到了十個(gè)10,下面我們用閉包修改這個(gè)代碼,使它可以產(chǎn)生0~9:
var arr = [];
for(var i = 0; i < 10; i++){
arr[i] = (function(i){
return function(){
return i;
};
})(i);
}
for(var j = 0; j < arr.length; j++){
console.log(arr[j]());
}//這樣就可以得到0~9了
當(dāng)然還以其他的解決方法:
//方法2
var arr = [];
for(var i = 0; i < 10; i++){
arr[i] = console.log.bind(null, i);
}
for(var j = 0; j < arr.length; j++){
console.log(arr[j]());
//方法3
var arr = [];
for(let i = 0; i < 10; i++){
arr[i] = function(){
console.log(i);
};
}
for(var j = 0; j < arr.length; j++){
console.log(arr[j]());
}//這樣也可以得到0~9了
迭代器
好了引谜,是時(shí)候放松一下了牍陌,看看下面這個(gè)代碼,這個(gè)會(huì)簡單一些
var inc = function(){
var x = 0;
return function(){
console.log(x++);
};
};
inc1 = inc();
inc1(); //0
inc1(); //1
inc2 = inc();
inc2(); //0
inc2(); //1
inc2 = null; //內(nèi)存回收
inc2 = inc();
inc2(); //0
你會(huì)發(fā)現(xiàn)员咽,inc返回了一個(gè)函數(shù)毒涧,這個(gè)函數(shù)是個(gè)累加器,它們可以獨(dú)立工作互補(bǔ)影響贝室。這個(gè)就是js中迭代器next()的實(shí)現(xiàn)原理契讲。下面是一個(gè)簡單的迭代器:
//實(shí)現(xiàn)對(duì)數(shù)組遍歷
function iterator(arr){
var num = 0;
return {
next: function(){
if(num < arr.length)
return arr[num++];
else return null;
}
};
}
var a = [1,3,5,7,9];
var it = iterator(a);
var num = it.next()
while(num !== null){
console.log(num)
num = it.next();
}//依次輸出1,3滑频,5捡偏,7峡迷,9
如果你學(xué)了ES6银伟,那么你可以用現(xiàn)成的迭代器,就不用自定義迭代器了绘搞。
箭頭函數(shù)
箭頭函數(shù)本身也是一個(gè)函數(shù)彤避,具有自己的作用域。不過在箭頭函數(shù)里面的this上下文同函數(shù)定義所在的上下文夯辖,具體可以看我的另一篇文章:javascript中this詳解
典型實(shí)例
這個(gè)實(shí)例會(huì)涉及到對(duì)象的相關(guān)知識(shí)忠藤,如果不能完全理解,可以參考:javascript中this詳解 和 javascript對(duì)象楼雹、類與原型鏈
function Foo() {
getName = function () { console.log (1); };
return this;
}
Foo.getName = function () { console.log (2);};
Foo.prototype.getName = function () { console.log (3);};
var getName = function () { console.log (4);};
function getName() { console.log (5);}
//請(qǐng)寫出以下輸出結(jié)果:
Foo.getName(); //2, 函數(shù)的靜態(tài)方法模孩,直接調(diào)用相關(guān)函數(shù)就可以了。
getName(); //4, 變量函數(shù)定義在調(diào)用之前贮缅,成功完成初始化榨咐,覆蓋函數(shù)聲明方式定義的同名函數(shù)
Foo().getName(); //1, 這里 Foo()返回的 this 是 window,在 Foo調(diào)用時(shí),對(duì)全局的變量型函數(shù) getName 重新定義了谴供,所以得到1块茁。
getName(); //1, 上一句改變了全局的 getName 函數(shù)為 cosnole.log(1)
new Foo.getName(); //2,無參數(shù) new 運(yùn)算比 . 運(yùn)算低桂肌,所以先運(yùn)行 Foo.getName数焊,得到2
new Foo().getName(); //3,有參數(shù) new 運(yùn)算和 . 運(yùn)算同一等級(jí)崎场,故從左到右佩耳,先運(yùn)算 new Foo() 得到一個(gè)匿名對(duì)象,在該對(duì)象上調(diào)用 getName 函數(shù)得到3
new new Foo().getName(); //3谭跨,同上干厚,先得到匿名對(duì)象李滴,然后將該對(duì)象的方法 getName 當(dāng)做構(gòu)造函數(shù)來調(diào)用,得到一個(gè)新對(duì)象蛮瞄,并輸出3;
Curry化
Curry化技術(shù)是一種通過把多個(gè)參數(shù)填充到函數(shù)體中所坯,實(shí)現(xiàn)將函數(shù)轉(zhuǎn)換為一個(gè)新的經(jīng)過簡化的(使之接受的參數(shù)更少)函數(shù)的技術(shù)。當(dāng)發(fā)現(xiàn)正在調(diào)用同一個(gè)函數(shù)時(shí)挂捅,并且傳遞的參數(shù)絕大多數(shù)都是相同的芹助,那么用一個(gè)Curry化的函數(shù)是一個(gè)很好的選擇.
下面利用閉包實(shí)現(xiàn)一個(gè)curry化的加法函數(shù)
function add(x,y){
if(x && y) return x + y;
if(!x && !y) throw Error("Cannot calculate");
return function(newx){
return x + newx;
};
}
add(3)(4); //7
add(3, 4); //7
var newAdd = add(5);
newAdd(8); //13
var add2000 = add(2000);
add2000(100); //2100