五、閉包

2021-03-02 不知非攻

文章最新內容已遷移至公眾號 不知非攻,後續維護也在公眾號裡修正,歡迎關注。

初學JavaScript時,我在閉包上,走了很多彎路。而這次重新回過頭來對基礎知識進行梳理,要講清楚閉包,也是一個非常大的挑戰。

閉包有多重要?如果你是初入前端的朋友,我沒有辦法直觀的告訴你閉包在實際開發中的無處不在,但是我可以告訴你,前端面試,必問閉包。面試官們常常用對閉包的了解程度來判定面試者的基礎水平,保守估計,10個前端面試者,至少5個都死在閉包上。

可是為什麼,閉包如此重要,還是有那麼多人沒有搞清楚呢?是因為大家不願意學習嗎?還真不是,而是我們通過搜索找到的大部分講解閉包的中文文章,都沒有清晰明了的把閉包講清楚。要麼淺嘗輒止,要麼高深莫測,要麼乾脆就直接亂說一通。

因此本文的目的就在於,能夠清晰明了的把閉包說清楚,讓讀者朋友們看了之後,就把閉包給徹底學會了,而不是似懂非懂。

閉包

對於有一點 JavaScript 使用經驗但從未真正理解閉包概念的人來說,理解閉包可以看作是某種意義上的重生,突破閉包的瓶頸可以使你功力大增。

閉包是一種特殊的對象。

它由兩部分組成。執行上下文(代號A),以及在該執行上下文中創建的函數(代號B)。

當B執行時,如果訪問了A中變量對象中的值,那麼閉包就會產生。

在大多數理解中,包括許多著名的書籍,文章裡都以函數B的名字代指這裡生成的閉包。而在chrome中,則以執行上下文A的函數名代指閉包。

因此我們只需要知道,一個閉包對象,由A、B共同組成,在以後的篇幅中,我將以chrome的標準來稱呼。

function foo() {  var a = 20;  var b = 30;
function bar() { return a + b; }
return bar;}
var bar = foo();bar();

上面的例子,首先有執行上下文foo,在foo中定義了函數bar,而通過對外返回bar的方式讓bar得以執行。當bar執行時,訪問了foo內部的變量a,b。因此這個時候閉包產生。

在基礎進階(一)[1]中,我總結了JavaScript的垃圾回收機制。JavaScript擁有自動的垃圾回收機制,關於垃圾回收機制,有一個重要的行為,那就是,當一個值,在內存中失去引用時,垃圾回收機制會根據特殊的算法找到它,並將其回收,釋放內存。

而我們知道,函數的執行上下文,在執行完畢之後,生命周期結束,那麼該函數的執行上下文就會失去引用。其佔用的內存空間很快就會被垃圾回收器釋放。可是閉包的存在,會阻止這一過程。

先來一個簡單的例子。

var fn = null;function foo() {  var a = 2;  function innnerFoo() {    console.log(a);  }  fn = innnerFoo; }
function bar() { fn(); }
foo();bar();

在上面的例子中,foo()執行完畢之後,按照常理,其執行環境生命周期會結束,所佔內存被垃圾收集器釋放。但是通過fn = innerFoo,函數innerFoo的引用被保留了下來,複製給了全局變量fn。這個行為,導致了foo的變量對象,也被保留了下來。於是,函數fn在函數bar內部執行時,依然可以訪問這個被保留下來的變量對象。所以此刻仍然能夠訪問到變量a的值。

這樣,我們就可以稱foo為閉包。

下圖展示了閉包foo的作用域鏈。

我們可以在chrome瀏覽器的開發者工具中查看這段代碼運行時產生的函數調用棧與作用域鏈的生成情況。如下圖。

關於如何在chrome中觀察閉包,以及更多閉包的例子,請閱讀基礎系列(六)

在上面的圖中,紅色箭頭所指的正是閉包。其中Call Stack為當前的函數調用棧,Scope為當前正在被執行的函數的作用域鏈,Local為當前的局部變量。

所以,通過閉包,我們可以在其他的執行上下文中,訪問到函數的內部變量。比如在上面的例子中,我們在函數bar的執行環境中訪問到了函數foo的a變量。個人認為,從應用層面,這是閉包最重要的特性。利用這個特性,我們可以實現很多有意思的東西。

不過讀者朋友們需要注意的是,雖然例子中的閉包被保存在了全局變量中,但是閉包的作用域鏈並不會發生任何改變。在閉包中,能訪問到的變量,仍然是作用域鏈上能夠查詢到的變量。

對上面的例子稍作修改,如果我們在函數bar中聲明一個變量c,並在閉包fn中試圖訪問該變量,運行結果會拋出錯誤。

var fn = null;function foo() {  var a = 2;  function innnerFoo() {    console.log(c);     console.log(a);  }  fn = innnerFoo; }
function bar() { var c = 100; fn(); }
foo();bar();

關於這一點,很多同學把函數調用棧與作用域鏈沒有分清楚,所以有的童鞋看了我關於介紹執行上下文的文章時就義正言辭的說我的例子有問題,而這些評論有很大的誤導作用,為了幫助大家自己擁有能夠辨別的能力,所以我寫了基礎(六),教大家如何在chrome中觀察閉包,作用域鏈,this等。當然我也不敢100%保證我文中的例子就一定正確,所以教大家如何去辨認我認為是一件最重要的事情。

閉包的應用場景

當然,只有把閉包運用到實踐中,才能對閉包有更深刻的認識。

這裡我們大概了解一下閉包的兩個非常重要的應用場景,他們分別是模塊化與柯裡化。

柯裡化

在函數式編程中,利用閉包能夠實現很多炫酷的功能,柯裡化便是其中很重要的一種。

具體的內容在後面的章節中詳細分析。

模塊化

模塊化是閉包最強大的一個應用場景。如果你是初學者,對於模塊的了解可以暫時不用放在心上,因為理解模塊需要更多的基礎知識。但是如果你已經有了很多JavaScript的使用經驗,在徹底了解了閉包之後,不妨藉助本文介紹的作用域鏈與閉包的思路,重新理一理關於模塊的知識。這對於我們理解各種各樣的設計模式具有莫大的幫助。

(function () {  var a = 10;  var b = 20;
function add(num1, num2) { var num1 = !!num1 ? num1 : a; var num2 = !!num2 ? num2 : b;
return num1 + num2; }
window.add = add;})();
add(10, 20);

在上面的例子中,我使用函數自執行的方式,創建了一個模塊。add是模塊對外暴露的一個公共方法。而變量a,b被作為私有變量。在面向對象的開發中,我們常常需要考慮是將變量作為私有變量,還是放在構造函數中的this中,因此理解閉包,以及原型鏈是一個非常重要的事情。模塊十分重要,因此我會在以後的文章專門介紹,這裡就暫時不多說啦。

為了驗證自己有沒有搞懂作用域鏈與閉包,這裡留下一個經典的思考題,常常也會在面試中被問到。

利用閉包,修改下面的代碼,讓循環輸出的結果依次為1, 2, 3, 4, 5

for (var i = 1; i <= 5; i++) {  setTimeout(function timer() {    console.log(i);  }, i * 1000);}

點此查看關於此題的詳細解讀[2]

理解閉包並不是一件簡單的事,如果感覺有困難,建議反覆閱讀。

References

[1] 基礎進階(一): http://www.jianshu.com/p/996671d4dcc4
[2] 點此查看關於此題的詳細解讀: http://www.jianshu.com/p/9b4a54a98660

相關焦點

  • 前端基礎進階(五):JavaScript 閉包詳細圖解
    而這次重新回過頭來對基礎知識進行梳理,要講清楚閉包,也是一個非常大的挑戰。閉包有多重要?如果你是初入前端的朋友,我沒有辦法直觀的告訴你閉包在實際開發中的無處不在,但是我可以告訴你,前端面試,必問閉包。面試官們常常用對閉包的了解程度來判定面試者的基礎水平,保守估計,10個前端面試者,至少5個都死在閉包上。
  • Python——五分鐘理解函數式編程與閉包
    由於在Python當中也是一切都是對象,如果我們 把閉包外層的函數看成是一個類的話,其實閉包和類區別就不大了,我們甚至可以給閉包返回的函數關聯函數,這樣幾乎就是一個對象了。和寫一個class相比,通過閉包的方法 運算速度會更快。原因比較隱蔽,是因為閉包當中沒有self指針,從而節省了大量的變量的訪問和運算,所以計算的速度要快上一些。但是閉包搞出來的偽對象是 不能使用繼承、派生等方法的,而且和正常的用法格格不入,所以我們知道有這樣的方法就可以了,現實中並不會用到。
  • 詳解 JavaScript 閉包
    (closure)是Javascript語言的一個難點,也是它的特色,很多高級應用都要依靠閉包實現。閉包的特性閉包有三個特性:1.函數嵌套函數2.函數內部可以引用外部的參數和變量3.參數和變量不會被垃圾回收機制回收閉包的定義及其優缺點閉包 是指有權訪問另一個函數作用域中的變量的函數,創建閉包的最常見的方式就是在一個函數內創建另一個函數
  • 詳解 js 閉包
    閉包的特性閉包有三個特性:1.函數嵌套函數2.函數內部可以引用外部的參數和變量3.參數和變量不會被垃圾回收機制回收閉包的定義及其優缺點閉包 是指有權訪問另一個函數作用域中的變量的函數,創建閉包的最常見的方式就是在一個函數內創建另一個函數,通過另一個函數訪問這個函數的局部變量
  • 詳解js閉包
    閉包的特性閉包有三個特性:1.函數嵌套函數2.函數內部可以引用外部的參數和變量3.參數和變量不會被垃圾回收機制回收閉包的定義及其優缺點閉包 是指有權訪問另一個函數作用域中的變量的函數,創建閉包的最常見的方式就是在一個函數內創建另一個函數
  • 理解閉包與內存洩漏
    被包裹的函數則稱為閉包函數,包裹的函數(外部的函數)則為閉包函數提供了一個閉包作用域,所以形成的閉包作用域的名稱為外部函數的名稱。函數提供的作用域,閉包作用域內有一個變量bar可以被閉包函數訪問到。其實,閉包函數是否被外部變量持有並不重要,形成閉包的必要條件就是,閉包函數(被包裹的函數)中必須要使用到外部函數中的變量。
  • python中的閉包
    首先來看閉包的概念:簡單來說,閉包的概念就是當我們在函數內定義一個函數時,這個內部函數使用了外部函數的臨時變量,且外部函數的返回值是內部函數的引用時,我們稱之為閉包。def outer(a, b): def inner(x): return a + x + b return innerm1 = outer(1, 4)m2 = outer(4, 9)print m1(10), m2(5)輸出:注意:python當中函數是一個對象,所以可以作為某個函數的返回結果使用閉包時
  • 聊聊Python中的閉包
    來源: rainyear 連結:https://github.com/rainyear/pytips/blob/master/Markdowns/2016-03-10-Scope-and-Closure.md(點擊尾部閱讀原文前往)閉包
  • python進階教程之閉包
    閉包1.什麼是閉包#定義一個函數def test(number):#在函數內部再定義一個函數,並且這個函數用到了外邊函數的變量,那麼將這個函數以及用到的一些變量稱之為閉包def test_in(number_in):print("in test_in 函數, number_in is %d"%number_in)return
  • 說說Python中閉包是什麼?
    廢話不多說,開始今天的題目:問:說說Python中閉包是什麼?答:可以將閉包理解為一種特殊的函數,這種函數由兩個函數的嵌套組成,外函數和內函數。在一個外函數中定義了一個內函數,內函數裡運用了外函數的臨時變量,並且外函數的返回值是內函數的引用。這樣就構成了一個閉包。
  • 原來JavaScript的閉包是這麼回事!
    讓我們看一個返回函數的函數的示例,因為這對理解閉包來說很重要。所以,肯定存在另一種被我們忽略的機制——也就是閉包。下面是它的工作原理。每當聲明一個新函數並將其賦值給變量時,實際上是保存了函數定義和閉包。閉包包含了創建函數時聲明的所有變量,就像一個背包一樣——函數定義附帶一個小背包。這個背包保存了創建函數時聲明的所有變量。
  • OC與Swift閉包對比總結
    Swift的閉包OC中的__block是一個很討厭的修飾符。它不僅不容易理解,而且在ARC和非ARC的表現截然不同。__block修飾符本質上是通過截獲變量的指針來達到在閉包內修改被截獲的變量的目的。在Swift中,這叫做截獲變量的引用。閉包默認會截取變量的引用,也就是說所有變量默認情況下都是加了__block修飾符的。
  • 什麼是閉包?一分鐘帶你了解!
    作者:茄果原文:www.cnblogs.com/qieguo/p/5457040.html什麼是閉包有權訪問另一個函數作用域內變量的函數都是閉包。這裡 inc 函數訪問了構造函數 a 裡面的變量 n,所以形成了一個閉包。
  • Python遞歸函數、閉包和裝飾器
    目錄:一、遞歸函數二、閉包的深入講解三、裝飾器的使用一、 遞歸函數sys.getrecursionlimit()3000設置遞歸深度sys.setrecursionlimit(500)sys.getrecursionlimit()500二、閉包
  • 第55p,閉包函數,函數知識的綜合運用
    大家好,我是楊數Tos,這是《從零基礎到大神》系列課程的第55篇文章,第三階段的課程:Python進階知識:Python進階知識:詳細講解Python中的函數(八)====> 函數的嵌套調用之閉包函數。
  • 用9種辦法解決 JS 閉包經典面試題之 for 循環取 i
    (上圖為 chrome 下 debug 環境)當在一個閉包域內包含另一個閉包域時(簡單的說就是在一個函數內有另一個函數,當然這個內部函數的生命周期是依附於外部函數的), 此時,若子閉包域(內部的閉包域,內部函數)使用了父閉包域(外部閉包域,外部函數)的私有變量(在父閉包域中聲明的變量,父閉包域的外部空間無法直接訪問,但子閉包域可以訪問),子閉包域即當前的子函數的 function
  • 解讀Python函數閉包的概念及作用域
    但在一些情況下,可以將函數內部的嵌套函數引入到全局環境中使用,Python將引入到全局環境中使用的嵌套函數及其環境變量構建成一個封閉的包,該包內的環境變量不受外部環境的影響,這就是我們將要討論的閉包。前面我們了解了嵌套函數的作用域僅限於其父函數體內,如果在父函數體外調用其嵌套的函數,就會超出嵌套函數的作用域。
  • Python內置函數、作用域、閉包、遞歸
    對於參數iterable中的每個元素都應用fuction函數,並返回一個map對象4.zip() #將對象逐一配對example:li =[1,2,3,4]sum(li) #10abs(-12) #絕對值 12round(1.4) #四捨五入 1round(1.445,2
  • 大部分人都會做錯的經典 JS 閉包面試題
    這是一道非常典型的JS閉包問題。其中嵌套了三層fun函數,搞清楚每層fun的函數是那個fun函數尤為重要。可以先在紙上或其他地方寫下你認為的結果,然後展開看看正確答案是什麼?遂:在第一次調用fun(0)時,o為undefined;第二次調用fun(1)時m為1,此時fun閉包了外層函數的n,也就是第一次調用的n=0,即m=1,n=0,並在內部調用第一層fun函數fun(1,0);所以o為0;第三次調用fun(2)時m為2,但依然是調用a.fun,所以還是閉包了第一次調用時的n,所以內部調用第一層的fun(2,0);
  • go 學習筆記之僅僅需要一個示例就能講清楚什麼閉包
    第一句我們知道了閉包是一種技術,而現在我們有知道了閉包存儲了閉包函數所需要的環境,而環境分為函數運行時所處的內部環境和依賴的外部環境,閉包函數被使用者調用時不會像普通函數那樣丟失環境而是存儲了環境.,所以對閉包函數自己來說就是自由的,並不受閉包函數的約束!