淺談匿名函數和閉包

前言

相信很多前端小伙伴在工作和學習中,都會或多或少的接觸和了解到匿名函數閉包。被這倆知識點所困擾,也去網上搜索了不少的資料,查到資料和解釋都各有說辭,甚至有些解釋本身就是不正確的,這更加讓人頭疼。今天就來聊一聊匿名函數閉包,淺談一下他們之間的關系(實際上他們之間并沒有什么直接關系!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上一位同學的回答截圖,我覺得他說的特別有道理:

image

原文地址: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);
}

可以得到跟使用立即執行函數同樣的效果,所以說匿名函數閉包之間并沒有什么關系,只不過很多時候在用到匿名函數解決問題的時候恰好形成了一個閉包,就導致很多人分不清楚匿名函數和閉包的關系。

至此,關于匿名函數和閉包的關系,也聊的差不多了,希望能給那些對匿名函數和閉包比較迷惑的小伙伴一些幫助,同時文章中有不足的地方,也請大伙給予指出,一起學習進步!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容

  • ● 閉包基礎 ● 閉包作用 ● 閉包經典例子 ● 閉包應用 ● 閉包缺點 ● 參考資料 1、閉包基礎 作用域和作...
    lzyuan閱讀 958評論 0 0
  • 函數作用域 要理解閉包,必須從理解函數被調用時都會發生什么入手。 我們知道,每個javascript函數都是一個對...
    黎貝卡beka閱讀 501評論 0 2
  • 本章將會介紹 閉包表達式尾隨閉包值捕獲閉包是引用類型逃逸閉包自動閉包枚舉語法使用Switch語句匹配枚舉值關聯值原...
    寒橋閱讀 1,569評論 0 3
  • 昨天沒有做引體向上,老婆夸我的肌肉不錯。其實我肚子上的肉也不錯。呵呵呵。沒有帶兒子睡覺,自己睡的特別早,因為前天晚...
    EKS語閱讀 130評論 0 0
  • 越來越多的眾籌項目在朋友圈中轉發,其中以大病眾籌為最多,而又以"每轉發一次,xx公司即支付x元"的文章獲得的轉發次...
    昌平女青年閱讀 182評論 1 0