前言
相信很多前端小伙伴在工作和學習中,都會或多或少的接觸和了解到匿名函數和閉包。被這倆知識點所困擾,也去網上搜索了不少的資料,查到資料和解釋都各有說辭,甚至有些解釋本身就是不正確的,這更加讓人頭疼。今天就來聊一聊匿名函數和閉包,淺談一下他們之間的關系(實際上他們之間並沒有什么直接關系!important)。
什么是匿名函數
與匿名函數相對應的是具名函數,具名函數非常簡單:function myFn(){},這就是個具名函數這個函數的name是myFn。可以測試一下:
function myFn(){ } cosnole.log(myFn.name);//myFn
特別說明一下,es6版本中引用型函數表達式也可以看成是具名函數。比如var myFn1 = function(){},打印myFn1.name,也會得到myFn1。
再說匿名函數,一般用到匿名函數的時候都是立即執行的。通常叫做自執行匿名函數或者自調用匿名函數。常用來構建沙箱模式,作用是開辟封閉的變量作用域環境,在多人聯合工作中,合並js代碼后,不會出現相同變量互相沖突的問題。立即執行的匿名函數有很多種寫法,常見的有以下兩種:
(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中每個函數都是一個閉包,這樣理解也是沒有問題的,不過會增加對閉包的理解難度,這里先不這么理解,可以按照閉包起的作用來理解它:就是能在一個函數外部執行這個函數內部定義的方法,並訪問這個函數內部定義的變量。
在此,先看個經典的使用閉包的案例,實現在函數外部訪問函數內部的局部變量:
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循環中:
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上一位同學的回答截圖,我覺得他說的特別有道理:

原文地址: https://stackoverflow.com/questions/8967214/what-is-the-difference-between-a-closure-and-an-anonymous-function-in-js。
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); }
可以得到跟使用立即執行函數同樣的效果,所以說匿名函數和閉包之間並沒有什么關系,只不過很多時候在用到匿名函數解決問題的時候恰好形成了一個閉包,就導致很多人分不清楚匿名函數和閉包的關系。
至此,關於匿名函數和閉包的關系,也聊的差不多了,希望能給那些對匿名函數和閉包比較迷惑的小伙伴一些幫助,同時文章中有不足的地方,也請大伙給予指出,一起學習進步!