相信不少前端小夥伴在工做和學習中,都會或多或少的接觸和了解到匿名函數和閉包。被這倆知識點所困擾,也去網上搜索了很多的資料,查到資料和解釋都各有說辭,甚至有些解釋自己就是不正確的,這更加讓人頭疼。今天就來聊一聊匿名函數和閉包,淺談一下他們之間的關係(實際上他們之間並無什麼直接關係!important)。javascript
與匿名函數相對應的是具名函數,具名函數很是簡單:function myFn(){},這就是個具名函數這個函數的name是myFn。能夠測試一下:前端
function myFn(){
}
cosnole.log(myFn.name);//myFn
複製代碼
特別說明一下,es6版本中引用型函數表達式也能夠當作是具名函數。好比var myFn1 = function(){},打印myFn1.name,也會獲得myFn1。java
再說匿名函數,通常用到匿名函數的時候都是當即執行的。一般叫作自執行匿名函數或者自調用匿名函數。經常使用來構建沙箱模式,做用是開闢封閉的變量做用域環境,在多人聯合工做中,合併js代碼後,不會出現相同變量互相沖突的問題。當即執行的匿名函數有不少種寫法,常見的有如下兩種:es6
(function(){
console.log("我是匿名方式1");
})();//我是匿名方式1
(function(){
console.log("我是匿名方式2");
}());//我是匿名方式2
console.log((function(){}).name);//'' name爲空
複製代碼
二者的區別就是:一個是發起執行的括號在匿名函數括號的外面,另一個發起執行的括號在匿名函數的裏面。實際中的書寫方式我的的話比較推薦第一種,這種寫法更符合調用機制,調用時的參數也比較明顯,以下:數組
(function(i,j,k){
console.log(i+j+k);
})(1,3,5);
//9
複製代碼
還有其餘一些自執行匿名函數的寫法,以下:閉包
-function(){
console.log("我是匿名方式x");
}();
console.log(-function(){}.name);//-0
+function(){
console.log("我是匿名方式x");
}();
console.log(+function(){}.name);//0
~function(){
console.log("我是匿名方式x");
}();
console.log(~function(){}.name);//-1
!function(){
console.log("我是匿名方式x");
}();
console.log(!function(){}.name);//true
void function(){
console.log("我是匿名方式x");
}();
console.log(void function(){}.name);//undefined
複製代碼
這幾種操做符,有時會影響結果的類型,不推薦使用,你們能夠查下資料看看各類方式之間的差異。具名函數其實也能夠當即執行,在此不作太多的伸展(本文主要目的是爲了說明匿名函數和閉包之間的關係)。函數
實際上,當即執行的匿名函數並非函數,由於已經執行過了,因此它是一個結果,只不過這個結果能夠是一個字符串、數字或者null/false/true,也能夠是對象、數組或者一個函數(對象和數組均可以包含函數),結果是什麼主要看函數執行完成時return什麼。學習
閉包自己定義比較抽象,MDN官方上解釋是:A closure is the combination of a function and the lexical environment within which that function was declared. 中文解釋是:閉包是一個函數和該函數被定義時的詞法環境的組合。 不少地方能夠看到一個說法:js中每一個函數都是一個閉包,這樣理解也是沒有問題的,不過會增長對閉包的理解難度,這裏先不這麼理解,能夠按照閉包起的做用來理解它:就是能在一個函數外部執行這個函數內部定義的方法,並訪問這個函數內部定義的變量。測試
在此,先看個經典的使用閉包的案例,實如今函數外部訪問函數內部的局部變量:ui
function box(){
var a = 10;
function inner(){
return a;
}
return inner;
}
var outer = box();
console.log(outer());//10
複製代碼
正常狀況,box執行事後,會被回收機制回收所佔用的內存,包括其內部定義的局部變量。可是此時box執行事後返回一個內部的函數inner,這個inner引用了內部的變量a,inner又被外部outer給接收,回收機制檢查到內部的變量被引用,就不會執行回收。
可是看到這裏,仍是一臉蒙比,哪裏使用了閉包?貌似有三個函數呀,一個box,一個inner還有一個outer = box()。
這個案例中用到的閉包實際上是inner和inner被定義時的詞法環境,這個閉包被return出來後被外部的outer引用,所以能夠在box外部執行這個inner,inner可以讀取到box內部的變量a。
使用這個閉包的目的是爲了在box外部訪問a,就是經過執行outer()。
上面的例子是在具名函數box內部用一個具名函數inner實現了閉包,那怎麼使用匿名函數實現閉包呢,也很簡單:
//第一步直把內部inner這個具名函數改成匿名函數並直接return, 結果一樣是10
function box(){
var a = 10;
return function(){
console.log(a) ;
}
}
var outer = box();
outer();//10
//第二步把外部var outer = box()改爲當即執行的匿名函數
var outer = (function(){
var a=10;
return function(){
console.log(a);
}
})();
//outer 做爲當即執行匿名函數執行結果的一個接收,這個執行結果是閉包,outer等於這個閉包。
//執行outer就至關於執行了匿名函數內部return的閉包函數
//這個閉包函數能夠訪問到匿名函數內部的私有變量a,因此打印出10
outer();//10
複製代碼
這樣咱們就改寫成了由匿名函數實現的閉包,真正使用到的閉包是內部的被return的函數和這個函數所定義時的環境。由此能夠說明:閉包跟函數是否匿名沒有直接關係,匿名函數和具名函數均可以建立閉包。
還有一個使人感到困惑,工做和學習中也常常碰見的問題是在for循環中:
for(var i = 0;i<5;i++){
setTimeout(function(){
console.log(i);
},100*i);
}
複製代碼
咱們但願打印出來0,1,2,3,4,然而打印出來的是5個5,很尷尬。什麼緣由引發的這問題呢?這是由於setTimeout的回調函數並非當即執行的而是要等到循環結束纔開始計時和執行(在此對運行機制不伸展),要說明的一點是js中函數在執行前都只對變量保持引用,並不會真正獲取和保存變量的值。因此等循環結束後i的值是已是5了,所以執行定時器的回調函數會打印出5個5。
1)怎麼解決這個問題? 最多見的解決方法就是給定時器外面加一個當即執行的匿名函數,並把當前循環的i做爲實參傳入這個當即執行的匿名函數。以下:
for(var i = 0;i<5;i++){
(function(i){
setTimeout(function(){
console.log(i);
},100*i);
})(i);
}
複製代碼
能夠獲得預想的結果:0,1,2,3,4,此時不少人認爲這個當即執行的匿名函數就是閉包,其實這麼理解是錯誤的,而後在錯誤的理解之上又擴展了好多案例,致使其餘人看後不知所謂,一頭霧水。附上一張Stack Overflow上一位同窗的回答截圖,我以爲他說的特別有道理:
2)那到底這個for循環中的閉包是什麼呢,其中的自執行匿名函數又起到什麼做用呢? 咱們能夠試着把這個自執行的匿名函數改寫爲具名的函數,來測試下結果:
for(var i = 0;i<5;i++){
function hasNameFn(i){
setTimeout(function(){
console.log(i);
},100*i);
};
hasNameFn(i);
}
複製代碼
能夠發現結果和使用匿名函數的結果是同樣的,因此這裏也能夠說明閉包跟匿名函數沒什麼直接關係。
這個for循環中的閉包怎麼理解以及自執行匿名函數的做用:
這個for循環實際上是在執行定時器的回調函數時才真正的產生了閉包,這些回調函數的執行環境是window,相似剛纔例子中的引用inner的全局outer的執行環境,匿名函數則至關於剛纔例子中的box函數。
而自執行的匿名函數的做用也很簡單:就是每一次循環建立一個私有詞法環境,執行時把當前的循環的i傳入,保存在這個詞法環境中,這個i就相似上面box函數中var聲明的局部變量a。
剛纔有說到函數在被執行前都只是保存對變量的引用,自執行的匿名函數正是由於執行了,因此可以獲取當前的變量i的值。所以定時器的回調函數在執行時引用的i就已經肯定了具體的值。
或許咱們改寫一下,這麼看就能更清晰明瞭一些:
for(var i = 0;i<5;i++){
(function(j){
var _i = j;
setTimeout(function(){
console.log(_i);
},100*_i);
})(i);
}
複製代碼
改寫後的匿名函數形參用j來表示,內部定義一個局部變量_i=j。匿名函數執行時傳入的是循環時的i,此時定時器裏面打印的_i實際上是j,匿名函數當即執行,j的值也會肯定。因此最後每次定時器的回調函數打印的結果也都是這個已經被匿名函數所肯定的值。
3)其餘的解決方案 解決剛纔for循環的問題,其實根本要解決的問題是如何讓每次循環的定時器的回調函數引用當前的i,而不是循環結束後的i。
最簡單的方法是使用es6 let,可以爲變量建立塊級做用域:
for(let i = 0;i<5;i++){
setTimeout(function(){
console.log(i);
},100*i);
}
//改寫成下面這麼寫更好理解一些
for(var i = 0;i<5;i++){
let j = i;
setTimeout(function(){
console.log(j);
},100*j);
}
複製代碼
還能夠用bind綁定當前的i給定時器的回調函數(實際上bind方法內部仍是實現了一個對調用者的柯里化閉包,並保存了執行時傳入的參數給調用者):
for(var i = 0;i<5;i++){
setTimeout(function(i){
console.log(i);
}.bind(this,i),100*i);
}
複製代碼
能夠獲得跟使用當即執行函數一樣的效果,因此說匿名函數和閉包之間並無什麼關係,只不過不少時候在用到匿名函數解決問題的時候剛好造成了一個閉包,就致使不少人分不清楚匿名函數和閉包的關係。
至此,關於匿名函數和閉包的關係,也聊的差很少了,但願能給那些對匿名函數和閉包比較迷惑的小夥伴一些幫助,同時文章中有不足的地方,也請大夥給予指出,一塊兒學習進步!