js之閉包
1逻卖、到底什么是閉包
閉包已經(jīng)成為近乎神話的概念粘捎,它非常重要又難以掌握,而且還難以定義丘侠。
1.1 古老的定義
閉包(closure)徒欣,是指函數(shù)變量可以保存在函數(shù)作用域內(nèi),因此看起來(lái)是函數(shù)將變量“包裹”了起來(lái)蜗字。
那這樣說(shuō)來(lái)打肝,包含變量的函數(shù)就是閉包。這個(gè)說(shuō)法已淘汰挪捕。
//按照古老定義粗梭,包含變量n的函數(shù)foo就是閉包
function foo() {
var n = 0;
}
console.log(n)//Uncaught ReferenceError: n is not defined
1.2 定義一
閉包是指可以訪問(wèn)其所在作用域的函數(shù)。
那這樣說(shuō)來(lái)级零,需要通過(guò)作用域鏈查找變量的函數(shù)就是閉包断医。也不是常規(guī)理解。
//按照定義一的說(shuō)法奏纪,需要通過(guò)作用域鏈在全局環(huán)境中查找變量n的函數(shù)foo()就是閉包
var n = 0;
function foo() {
console.log(n); // 0
}
foo();
1.3 定義二
閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù)鉴嗤。
那這樣說(shuō)來(lái),訪問(wèn)上層函數(shù)的作用域的內(nèi)層函數(shù)就是閉包序调。
//按照定義二的說(shuō)法躬窜,嵌套在foo函數(shù)里的bar函數(shù)就是閉包
function foo(){
var a = 2;
function bar(){
console.log(a); // 2
}
bar();
}
foo();
1.4 定義三
閉包是指在函數(shù)聲明時(shí)的作用域以外的地方被調(diào)用的函數(shù)。
在函數(shù)聲明時(shí)的作用域以外的地方調(diào)用函數(shù)炕置,需要通過(guò)將該函數(shù)作為返回值或者作為參數(shù)被傳遞荣挨。
1男韧、返回值
//按照定義三的說(shuō)法,在foo()函數(shù)的作用域中聲明默垄,在全局環(huán)境的作用域中被調(diào)用的bar()函數(shù)是閉包
function foo(){
var a = 2;
function bar(){
console.log(a); //2
}
return bar;
}
foo()();
function foo(){
var a = 2;
return function(){
console.log(a);//2
}
}
foo()();
2此虑、參數(shù)
//按照定義三的說(shuō)法,在foo()函數(shù)的作用域中聲明口锭,在bar()函數(shù)的作用域中被調(diào)用的baz()函數(shù)是閉包
function foo(){
var a = 2;
function baz(){
console.log(a); //2
}
bar(baz);
}
function bar(fn){
fn();
}
因此朦前,無(wú)論通過(guò)何種手段,只要將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外鹃操,它都會(huì)持有對(duì)原始作用域的引用韭寸,無(wú)論在何處執(zhí)行這個(gè)函數(shù)都會(huì)使用閉包。
1.5 IIFE
IIFE(Immediately-Invoked Function Expression)(立即執(zhí)行函數(shù)表達(dá)式)是不是閉包呢荆隘?
foo()函數(shù)在全局作用域定義恩伺,也在全局作用域被立即調(diào)用,如果按照定義一的說(shuō)法來(lái)說(shuō)椰拒,它是閉包晶渠。如果按照定義二和定義三的說(shuō)法,它又不是閉包燃观。
var a = 2;
(function foo(){
console.log(a);//2
})();
1.6 總結(jié)
閉包定義之所以混亂褒脯,我覺(jué)得與經(jīng)典書籍的不同解讀有關(guān)。經(jīng)典定義是犀牛書的原話缆毁,定義二是高程的原話番川。
這里參考阮一峰老師的理解,原文鏈接學(xué)習(xí)javascript中的閉包脊框。
閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)颁督。
由于在Javascript語(yǔ)言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量缚陷,因此可以把閉包簡(jiǎn)單理解成"定義在一個(gè)函數(shù)內(nèi)部的函數(shù)"适篙。
所以往核,在本質(zhì)上箫爷,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來(lái)的一座橋梁。
他的理解主要從作用或用途去直接的表達(dá)聂儒。小火柴(參考博文博主)想從原理上去表達(dá)閉包函數(shù)虎锚。
但,歸納起來(lái)就是關(guān)于一個(gè)函數(shù)要成為一個(gè)閉包到底需要滿意幾個(gè)條件衩婚。
嚴(yán)格來(lái)說(shuō)窜护,閉包需要滿足三個(gè)條件:
- 訪問(wèn)所在作用域;
- 函數(shù)嵌套非春;
- 在所在作用域外被調(diào)用柱徙。
有些人覺(jué)得只滿足條件1就可以缓屠,所以IIFE是閉包;有些人覺(jué)得滿足條件1和2才可以护侮,所以被嵌套的函數(shù)才是閉包敌完;有些人覺(jué)得3個(gè)條件都滿足才可以,所以在作用域以外的地方被調(diào)用的函數(shù)才是閉包羊初。
問(wèn)題是滨溉,誰(shuí)是權(quán)威呢?
2长赞、IIFE
IIFE(Immediately-Invoked Function Expression)(立即執(zhí)行函數(shù)表達(dá)式)晦攒。
2.1 實(shí)現(xiàn)
函數(shù)跟隨一對(duì)圓括號(hào)()表示函數(shù)調(diào)用。
//函數(shù)聲明語(yǔ)句寫法
function test(){};
test();
//函數(shù)表達(dá)式寫法
var test = function(){};
test();
但有時(shí)需要在定義函數(shù)之后得哆,立即調(diào)用該函數(shù)脯颜。這種函數(shù)就叫做立即執(zhí)行函數(shù),全稱為立即調(diào)用的函數(shù)表達(dá)式IIFE(Imdiately Invoked Function Expression)柳恐。
javascript引擎規(guī)定伐脖,如果function關(guān)鍵字出現(xiàn)在行首,一律解釋成函數(shù)聲明語(yǔ)句乐设。
1讼庇、函數(shù)聲明語(yǔ)句需要一個(gè)函數(shù)名,由于沒(méi)有函數(shù)名近尚,所以下面報(bào)錯(cuò)蠕啄。
//SyntaxError: Unexpected token (
function(){}();
2、函數(shù)聲明語(yǔ)句后面加上一對(duì)圓括號(hào)戈锻,只是函數(shù)聲明語(yǔ)句與分組操作符的組合而已歼跟。由于分組操作符不能為空,所以下面報(bào)錯(cuò)格遭。
//SyntaxError: Unexpected token )
function foo(){}();
//等價(jià)于
function foo(){};
();//SyntaxError: Unexpected token )
3哈街、函數(shù)聲明語(yǔ)句加上一對(duì)有值的圓括號(hào),也僅僅是函數(shù)聲明語(yǔ)句與不報(bào)錯(cuò)的組合而已拒迅。
function foo(){}(1);
//等價(jià)于
function foo(){};
(1);
解決方法就是不要讓function出現(xiàn)在行首骚秦,讓引擎將其理解成一個(gè)表達(dá)式。
常用的兩種辦法這樣:
(function(){ /* code */ }());
(function(){ /* code */ })();
其他的寫法:
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();
new function(){ /* code */ };
new function(){ /* code */ }();
2.2 作用域
對(duì)于IIFE來(lái)說(shuō)璧微,通過(guò)作用域鏈來(lái)查找變量與普通函數(shù)有一些不同的地方作箍。
2.2.1 with
with語(yǔ)句中的IIFE會(huì)先在with語(yǔ)句中查找,然后再向上查找前硫。在下列代碼中胞得,標(biāo)準(zhǔn)瀏覽器下f()函數(shù)和IIFE都返回'bar',但I(xiàn)E10-瀏覽器中的f()函數(shù)返回'abc'屹电。
var foo = "abc";
with({
foo:"bar"
}){
function f(){
console.log(foo);
};
(function(){
console.log(foo);
})();
f();
}
2.2.2 try-catch
在下列代碼中阶剑,標(biāo)準(zhǔn)瀏覽器下f()函數(shù)和IIFE都返回'error'跃巡,但I(xiàn)E10-瀏覽器中的f()函數(shù)返回'10'。
try{
var e = 10;
throw new Error();
}catch(e){
function f(){
console.log(e);
}
(function (){
console.log(e);
})();
f();
}
2.2.3 具名函數(shù)表達(dá)式
下面的代碼牧愁,在標(biāo)準(zhǔn)瀏覽器中a()函數(shù)返回1瓷炮,而IIFE返回a函數(shù)代碼。但I(xiàn)E8瀏覽器全部返回1递宅。
function a(){
a = 1;
console.log(a);
};
a();
(function a(){
a = 1;
console.log(a);
})();
2.3 用途
IIFE一般用于構(gòu)造私有變量娘香,避免全局污染。
接下來(lái)办龄,下面有一個(gè)更直觀的需求來(lái)說(shuō)明IIFE的用途烘绽。假設(shè)有一個(gè)需求,每次調(diào)用函數(shù)俐填,都返回加1的一個(gè)數(shù)字(數(shù)字初始值為0)安接。
1、全局變量
一般情況下英融,我們會(huì)使用全局變量來(lái)保存該數(shù)字狀態(tài)
var a = 0;
function add(){
return ++a;
}
console.log(add());//1
console.log(add());//2
2盏檐、上面的方法局限在于變量a只和add函數(shù)有關(guān),卻聲明為全局變量驶悟,不太合適胡野。
將變量a更改為函數(shù)的自定義屬性更為恰當(dāng)。
function add(){
return ++add.count;
}
add.count = 0;
console.log(add());//1
console.log(add());//2
3痕鳍、然后這樣做可能還會(huì)存在問(wèn)題硫豆,有些時(shí)代代碼可能會(huì)無(wú)意間將add.count重置。使用IIFE把計(jì)數(shù)器變量保存為私有變量更安全笼呆,同時(shí)也減少全局變量的污染熊响。
var add = (function(){
var counter = 0;
return function(){
return ++counter;
}
})();
console.log(add())//1
console.log(add())//2
2.4 注意事項(xiàng)
IIFE稱為立即執(zhí)行函數(shù)州刽,這個(gè)立即執(zhí)行函數(shù)有多立即呢埂陆?
立即執(zhí)行函數(shù)再快也得按照代碼執(zhí)行順序去逐行執(zhí)行漆枚。
var a = 1;
(function(){
console.log(a);//1
})();
類似的忠烛,函數(shù)也是如此。
function a(){
return 1;
}
(function(){
console.log(a());//1
})();
但冰沙,如果是函數(shù)表達(dá)式就不一樣了迅诬。執(zhí)行代碼如下箭养,會(huì)報(bào)錯(cuò)奥喻,提示a的值是undefined偶宫。
var a = function(){
return 1;
}
(function(){
console.log(a());//報(bào)錯(cuò)
})();
函數(shù)有一個(gè)函數(shù)聲明提前hoisting的過(guò)程非迹,函數(shù)表達(dá)式其實(shí)分為先聲明后賦值這兩步环鲤。而,如果后者存在立即執(zhí)行函數(shù)表達(dá)式憎兽,這個(gè)IIFE會(huì)快帶函數(shù)表達(dá)式a執(zhí)行完第一步函數(shù)聲明后IIFE就會(huì)立即執(zhí)行冷离,此時(shí)a未被賦值吵冒,是undefined,所以執(zhí)行a()時(shí)會(huì)報(bào)錯(cuò)西剥。
3痹栖、常見(jiàn)的一個(gè)循環(huán)和閉包錯(cuò)誤的理解
關(guān)于常見(jiàn)的一個(gè)循環(huán)和閉包的錯(cuò)誤,很多資料都對(duì)此有文字解釋瞭空,但還是難以理解揪阿,本文將以執(zhí)行環(huán)境圖示的方式來(lái)對(duì)此進(jìn)行更直觀的解釋,以及對(duì)此需求進(jìn)行推衍咆畏,得到合適的解決方法南捂。
3.1 犯錯(cuò)
function foo(){
var arr = [];
for(var i = 0; i < 2; i++){
arr[i] = function(){
return i;
}
}
return arr;
}
var bar = foo();
console.log(bar[0]());//2
以上代碼的運(yùn)行結(jié)果是2,而不是預(yù)想的0旧找。接下來(lái)用執(zhí)行環(huán)境圖示的方法溺健,詳解到底是哪里出了問(wèn)題。
執(zhí)行流首先創(chuàng)建并進(jìn)入全局執(zhí)行環(huán)境钮蛛,進(jìn)行聲明提前的過(guò)程鞭缭。執(zhí)行流執(zhí)行到第10行,創(chuàng)建并進(jìn)入foo()函數(shù)執(zhí)行環(huán)境魏颓,并進(jìn)行其詞法作用域內(nèi)的聲明提前岭辣。然后執(zhí)行第2行,將arr賦值為[]甸饱。然后執(zhí)行到第3行易结,給arr[0]和arr[1]都賦值一個(gè)匿名函數(shù)。然后執(zhí)行到第8行柜候,以arr的值為返回值退出函數(shù)搞动。由于此時(shí)有閉包的存在,所以foo()的執(zhí)行環(huán)境并不會(huì)被銷毀渣刷,且i的值被保存鹦肿。
執(zhí)行流進(jìn)入全局執(zhí)行變量,繼續(xù)執(zhí)行第10行辅柴,將函數(shù)的返回值arr賦值給bar箩溃。
執(zhí)行流執(zhí)行到第11行,訪問(wèn)bar的第0個(gè)元素并執(zhí)行碌嘀。此時(shí)涣旨,執(zhí)行流創(chuàng)建并進(jìn)入匿名函數(shù)執(zhí)行環(huán)境,匿名函數(shù)中存在自由變量i股冗,需要使用其作用域鏈匿名函數(shù)->foo()函數(shù)->全局作用域進(jìn)行查找霹陡,最終在foo()函數(shù)的作用域找到了i,然后在foo()的執(zhí)行環(huán)境找到了i的值為2,于是給賦值為2烹棉。
執(zhí)行流接著執(zhí)行第5行攒霹,以i的值2作為返回值返回。同時(shí)銷毀匿名函數(shù)的執(zhí)行環(huán)境浆洗。執(zhí)行流進(jìn)入全局執(zhí)行環(huán)境催束,接著執(zhí)行第11行,調(diào)用內(nèi)部對(duì)象console伏社,并找到其方法log抠刺,將bar0的值2作用參數(shù)放入該方法中,最終在控制臺(tái)顯示2摘昌。
由此我們看出矫付,犯錯(cuò)的原因主要在循環(huán)的過(guò)程中,并沒(méi)有把函數(shù)的返回值賦值給數(shù)組元素第焰,而僅僅把函數(shù)賦值給了數(shù)組元素买优。這就使得在調(diào)用匿名函數(shù)時(shí),通過(guò)作用域找到的執(zhí)行環(huán)境中存儲(chǔ)變量的值已經(jīng)不是循環(huán)時(shí)的瞬時(shí)索引值挺举,二十循環(huán)執(zhí)行完畢之后的索引值杀赢。
3.2 IIFE
由此,可以利用IIFE傳參和閉包來(lái)創(chuàng)建多個(gè)執(zhí)行環(huán)境來(lái)保存循環(huán)時(shí)各個(gè)狀態(tài)的索引值湘纵。因?yàn)楹瘮?shù)傳參是按值傳遞的脂崔,不同的參數(shù)的函數(shù)調(diào)用時(shí),會(huì)創(chuàng)建不同的執(zhí)行環(huán)境梧喷。
function foo(){
var arr = [];
for(var i = 0; i < 2; i++){
arr[i] = (function fn(j){
return function test(){
return j;
}
})(i);
}
return arr;
}
var bar = foo();
console.log(bar[0]());//0
3.3 塊作用域
使用IIFE還是較為復(fù)雜砌左,使用作用域塊則更為方便。
由于塊作用域可以將索引值i重新綁定到了循環(huán)的每一個(gè)迭代中铺敌,確保使用上一個(gè)循環(huán)迭代結(jié)束時(shí)的值重新進(jìn)行賦值汇歹,相當(dāng)于每一次索引值都創(chuàng)建一個(gè)執(zhí)行環(huán)境。
function foo(){
var arr = [];
for(let i = 0; i < 2; i++){
arr[i] = function(){
return i;
}
}
return arr;
}
var bar = foo();
console.log(bar[0]());//0
在編程中偿凭,如果實(shí)際和預(yù)期結(jié)果不符产弹,就按照代碼順序一步一步地把執(zhí)行環(huán)境圖示畫出來(lái),會(huì)發(fā)現(xiàn)很多時(shí)候就是在想當(dāng)然弯囊!
4痰哨、閉包的7中形式
根據(jù)閉包的定義,我們知道匾嘱,無(wú)論通過(guò)各種手段斤斧,只要將內(nèi)部函數(shù)傳遞到詞法作用域以外,它都會(huì)持有對(duì)原有作用域的引用霎烙,無(wú)論在何處執(zhí)行這個(gè)函數(shù)都會(huì)使用閉包撬讽,下面會(huì)介紹閉包的7種形式蕊连。
4.1 返回值
最常用的形式是函數(shù)作為函數(shù)值被返回:
var F = function(){
var b = 'local';
var N = function(){
return b;
}
return N;
}
console.log(F()());
4.2 函數(shù)賦值
一種變形的形式是將內(nèi)部函數(shù)賦值給一個(gè)外部變量:
var inner;
var F = function(){
var b = 'local';
var N = function(){
return b;
};
inner = N;
};
F();
console.log(inner());
4.3 函數(shù)參數(shù)
閉包可以通過(guò)函數(shù)參數(shù)傳遞函數(shù)的形式來(lái)實(shí)現(xiàn):
var Inner = function(fn){
console.log(fn());
}
var F = function(){
var b = 'local';
var N = function(){
return b;
}
Inner(N);
}
F();
4.4 IIFE
前面的代碼實(shí)例可知,函數(shù)F()都是在聲明之后立即被調(diào)用锐秦,因此可以使用IIFE來(lái)代替。但是盗忱,需要注意的是酱床,這里的Inner()只能用函數(shù)聲明語(yǔ)句的形式,而不能用函數(shù)表達(dá)式趟佃。
function Inner(fn){
console.log(fn());
}
(function(){
var b = 'local';
var N = function(){
return b;
}
Inner(N);
})();
4.5 循環(huán)賦值
在閉包問(wèn)題上扇谣,最常見(jiàn)的一個(gè)錯(cuò)誤就是循環(huán)賦值的錯(cuò)誤。其錯(cuò)誤的原因在之前閉包環(huán)節(jié)已經(jīng)講述闲昭。
錯(cuò)誤:
function foo(){
var arr = [];
for(var i = 0; i < 2; i++){
arr[i] = function(){
return i;
}
}
return arr;
}
var bar = foo();
console.log(bar[0]());//2
正確:
function foo(){
var arr = [];
for(var i = 0; i < 2; i++){
arr[i] = (function fn(j){
return function test(){
return j;
}
})(i);
}
return arr;
}
var bar = foo();
console.log(bar[0]());//0
4.6 g(s)etter
我們通過(guò)提供getter()函數(shù)和setter()函數(shù)老將要操作的變量保存在函數(shù)內(nèi)部罐寨,防止其暴露在外部。
var getValue,setValue;
(function(){
var secret = 0;
getValue = function(){
return secret;
}
setValue = function(v){
if(typeof v === 'number'){
secret = v;
}
}
})();
console.log(getValue());//0
setValue(1);
console.log(getValue());//1
4.7 迭代器
我們經(jīng)常使用一個(gè)閉包來(lái)實(shí)現(xiàn)一個(gè)累加器序矩。
var add = (function(){
var counter = 0;
return function(){
return ++counter;
}
})();
console.log(add())//1
console.log(add())//2
類似地鸯绿,使用閉包可以很方便的實(shí)現(xiàn)一個(gè)迭代器。
function setup(x){
var i = 0;
return function(){
return x[i++];
}
}
var next = setup(['a','b','c']);
console.log(next());//'a'
console.log(next());//'b'
console.log(next());//'c'
全文博文地址:閉包簸淀。