深刻學習 JavaScript —— 閉包

前言

做者雖然不是第一次寫學習筆記了,可是系統性的專欄確實是第一回寫,因此對這篇文章難度的預估量不夠,醞釀了蠻久時間的。寫以前一直在糾結一個問題,這類問題網上已經有各種大神作出各類很詳細的分解了,還有必要寫嗎? 能讓我有動力寫的緣由,一個是強烈的表達慾望,另外一個是但願後來者能經過我這篇文章學到一點東西。基於這兩點,我這篇文章的思路就有了:要寫得儘量詳細甚至囉嗦,多用一些生活化的例子做比。固然,我這個行文思路也是參考借鑑了【張鑫旭】大神的。好了,接下來開啓個人第一篇文章吧——關於 JavaScript 的閉包 (closures)。javascript

正文前再囉嗦點什麼

我但願看到這篇文章的人可以秉持一個思惟習慣:和物理、數學等客觀存在的知識點不同的是,編程領域的知識點有至關大的部分具備主觀性——意思就是,不少編程領域的知識首先是人根據現實的需求創造出來的,它不是客觀就存在的。這就致使編程領域的一個現象:對於一種問題的解法,咱們能夠有不少不一樣角度的思路,這其中前端領域尤其突出。但它們都是人類所創造出來的概念。 這個思惟會有助於咱們去學習編程領域的知識點(至少我是這樣)。回到咱們這篇文章的主題,閉包,這個令許多 JS 新手頭疼不已的問題,其實只是人們解決函數式編程問題用到的一個概念工具。什麼是函數式編程?這個概念比閉包還難說清楚,計劃專研後再後續寫些文章詳細總結。 如今咱們就簡單地把函數式編程與面向對象、面向過程編程視爲同一層次的概念,它是一種編程的思惟模式。而閉包只是實現函數式編程的高效工具之一。囉嗦了那麼多,來看張明白直了的圖吧~ html

圖例

閉包的定義

上面說到閉包是實現函數式編程的工具之一。可是,閉包究竟是什麼,長什麼樣?《你不知道的 JavaScript》書中,將閉包定義爲【函數在所定義的做用域以外的區域被調用】。聽上去有點繞,我舉個比較現實的例子:前端

閉包的函數定義與使用做用域分離,有點相似古代對軍隊的掌控和調度權力分離。在大多數狀況下,古代軍隊是隻忠於皇廷的;一旦恰逢大戰,皇帝很差御駕親征,這時會將軍隊的調度權臨時給予將領。將領真正掌控了軍隊嗎?正常狀況下是沒有的,他只有在沙場上調度軍隊的權力。咱們能夠看到,軍隊明面的掌管者是誰?皇帝。但實際的操縱者是誰?將領。 這種權力分離的過程是如何實現的呢?你可能知道的,虎符!虎符是實現這種權力傳遞,或者分離的概念性工具。java

從某種意義上看,閉包和虎符同樣,也是一種概念性工具。如今是否是可以勉強記住上面那句,【函數在所定義的做用域以外的區域被調用】?若是能夠的話,如今舉兩個例子,請你判斷是否爲閉包,以及是否輸出正常。最好思考一分鐘,而後再看解釋。編程

// example 1
function outter () {
    console.log("outter");
    return function inner () {
        console.log("inner");
    };
}
var p = outter();
p();
複製代碼
// example 2
function outter () {
    console.log("outter");
    function inner () {
        console.log("inner");
    }
    foo(inner);
}
function foo (fn) {
    fn();
}
outter();
複製代碼

上面兩個例子,你有什麼見解?事實上,這兩個都是《你不知道的 JavaScript》裏說起的閉包例子。 第一個例子,outterinner做爲返回值傳遞給pp在全局做用域執行時,inner的定義所在做用域與執行做用域不一樣,符合上面提到的閉包定義。 第二個例子,inner做爲參數被傳遞給foo執行,一樣的咱們能夠看到,inner的定義所在域與執行域不一樣,雖然它們都是outter函數做用域的子集。 不知看到此處的你,對閉包的理解是否清晰些了?若是你以前看過其它博客文章,它們對閉包的定義可能與本文不一致。好比說,在阮一峯這篇博文裏,對閉包的定義是【可以讀取其它函數內部變量的函數】。可能網上還有其它的定義。事實上,這些定義本質上都是同樣的,它們只不過從某個特定角度描述閉包的特性。就像虎符,雖然在各個朝代都有出現,可是它在每一個時期的形狀是不盡相同的。挑一個你認爲最好記的,加上輔助理解的實例就好。bash

閉包的做用

實際上,我更願意將阮一峯對閉包的定義——【可以讀取其它函數內部變量的函數】,視爲閉包的做用之一。可是,它的做用遠不止這個。 回到以前說起的函數式編程,它的一大特徵就是【函數是一等公民】。這句話怎麼理解?大概地說,函數式編程提倡用函數來實現,傳統面向對象中只有類和對象才能實現的功能。以前提到,閉包是實現函數式編程的工具之一,其實用處主要就在這裏。 閉包如何實現函數式編程呢?其實很簡單,既然閉包可於讀取函數的內部變量,換個角度想就是,閉包能夠實現函數變量或方法的公有化。有些同窗可能知道,傳統面向對象中,只有對象纔有私有和共有變量、方法之分,函數內部不只不能定義函數,也不能主動暴露內部變量。JS 中函數內部是能夠定義函數的,至關於有了內部方法。有了閉包,JS 中的函數就和對象很像啦,這就很符合函數式編程的理念了~ 然而,說了那麼多幹巴巴的定義,對於函數如何當對象使用,你可能仍是很懵。不要緊,舉幾個實例來輔助說明。閉包

// example 3
function people (_name, _age) {
    var name = _name;
    var age = _age;
    return function get () {
        console.log('name: ' + name);
        console.log('age: ' + age);
    };
}
var get_info = people("Lee", 20);
get_info();   // name: Lee; age: 20
複製代碼

上述例子在必定程度上體現了函數被看成對象的特色。然而,有人可能會鑽一個牛角尖:我已經知道它返回的是一個函數了,你只不過是在執行這個函數而已。首先恭喜你,能提出這個疑問,表明你至少看懂上一節內容了;其次,這個示例的重點不在函數的執行,而在這個函數輸出了不屬於它的變量數據。這在某種程度上,上文也有提到,實現函數變量或方法的公有化。 對 JS 有點了解的可能會問:JS 中不是有垃圾回收機制嗎,爲何people執行後它的內部變量沒有被回收?這個問題問得至關好,有一段時間我也很疑惑這點。下面再舉一個很經典的計數器例子,咱們來看看爲何會這樣。函數式編程

// example 4
function counter () {
    var count = 0;
    console.log("init: " + count++);
    return function increase () {
        count += 2;
        console.log("increase: " + count);
    }
}
var increase1 = counter();    // init: 0
var increase2 = counter();    // init: 0
increase1();    // increase: 3
increase1();    // increase: 5
increase2();    // increase: 3
複製代碼

這個例子是計數器的變形,之因此在counter內部域中作一個輸出,是爲了觀察執行increase時是否會執行counter內部的語句。 能夠看到,其一,increase執行時只用到counter的變量,並不執行某些特定的語句。但假如引用了另外一個函數m,確定會執行m內部的語句。其二,在代碼的運行週期內,count變量一直保留;只要願意,能夠執行若干次increase。這不符合 JS 垃圾回收的機制。其三,兩個變量 (increase1&increase2) 引用的計時器變量count不一樣。在 JS 語境下,它們實際上是兩個對象。 上述疑問中,一你們想一下就能明白了,三不是本文的重心,之後會再提到,重點是疑問二。這裏其實涉及到 JS 的函數傳遞和詞法做用域。在 JS 中,【函數是一等公民】還體如今:函數能夠做爲參數、返回值等進行傳遞。上述示例中,increase1獲取了counter返回的函數increase,此時這個函數保留有原父函數counter內部變量的引用。爲了代碼引用非空,JS 對此會進行特殊處理,【保留原父函數的詞法域】,但不會再執行。函數

總結

寫到這裏,這篇囉裏巴嗦的文章總算是到尾聲了。如今來作一個總結吧~ 本文咱們認識了閉包,那麼本文對閉包的定義是什麼?這裏不打出來,你本身小聲默唸一遍,想不起來就想一想虎符的例子。 第二個,閉包的做用?最大的做用固然是保證【函數是一等公民】的地位啦。可是這個太抽象了,回想一下例子,咱們能夠獲得兩個特殊做用,或者說特色。工具

  • 可以用於讀取函數內部變量和方法,模擬對象
  • 保留父函數的詞法域。須要注意的是,濫用這個特色會致使內存泄露。

參考

學習 JavaScript 閉包 —— 阮一峯 到底什麼是閉包 —— 知乎用戶 Agile2 《你不知道的 JavaScript(上卷)》

相關文章
相關標籤/搜索