@[toc]
不少人學JS剛學到閉包的時候會比較懵,特別是從強類型語言(如Java)轉而學JS的人,更是以爲這都啥跟啥呀。本文也就只針對這些剛學的新手,因此不會去談閉包的原理,只談閉包的基本使用,新手能夠放心食用。只有在知道如何使用以後,你再深刻了解就會駕輕就熟,在用都不知道用的狀況下就想對一個知識點了解的很透徹,這是不可能的。javascript
瞭解閉包的使用以前,先得捋清一下一些基本的知識點,我們一個知識點一個知識點慢慢往下捋,到最後你就會發現你已經知道如何使用閉包了。java
JS中,函數內聲明的變量其做用域爲整個函數體,在函數體外不可引用該變量,聽起來很玄乎,一看代碼你們就很清楚了:python
function outter() { // 在函數內部聲明一個變量 let x = 3; // 在函數體內使用這個變量,這個確定沒有什麼問題 console.log('我在本函數裏使用變量:' + x); } outter(); // 輸出內容:我在本函數裏使用變量:3 console.log(x); // 報錯,由於函數外部這樣拿不到函數內部的變量
這個知識點相信你們均可以理解,我在這裏說的再淺顯一些,函數內部聲明的變量,就只能在聲明該變量的大括號內使用,大括號外就用不了。因此看函數內的變量做用域,直接找大括號就行了。設計模式
咱們再繼續前進,因爲JS的函數能夠嵌套,此時內部函數能夠訪問外部函數定義的變量,反過來則不行:數組
// 外部函數 function outter() { let x = 3; // 內部函數 function inner() { let y = x + 1; // 內部函數能夠訪問外部函數的變量x } let z = y + 1; // 報錯,外部函數訪問不了內部函數的變量y }
這一點也很好理解,和前面一個知識點是徹底一致的,內部函數inner()
由於在外部函數outter()
的大括號內,固然就可使用變量x,而外部函數outter()
在內部函數inner()
的大括號外面,天然就用不了變量y。閉包
瞭解上面的基本知識點後就能夠開始瞭解閉包了。假設如今咱們有一個需求,我就是想在outter()
外面拿到變量x怎麼辦? 好辦呀,直接在outter()
裏將x當作返回值返回就行了:函數
function outter() { let x = 3; return x; } let y = outter(); // 3
OK,這樣是拿到了變量x,可是,嚴格的來講這只是拿到了變量的值,並無拿到變量。啥意思呢,就是說你沒法對變量x的值進行修改,若是我想將變量x的值自增1呢?你是沒法修改的,你就算修改變量y的值,x的值也不會被改變:設計
function outter() { let x = 3; return x; } let y = outter(); // 3 y++; console.log(y); // 4, y的值確實被修改了 console.log(outter()); // 3, 函數內部x並無被修改
有可能你會想到,那我在函數內部將x自增,而後再返回不就能夠了?日誌
function outter() { let x = 3; x++; return x; } console.log(outter()); // 4
OK,沒問題,可是我想每次調用函數的時候,x都會自增,就像一個計數器同樣,x的值會隨着個人調用次數動態增長。咱們能夠按照上面的代碼來演示一下看可否達到要求:code
function outter() { let x = 3; x++; return x; } console.log(outter()); // 4 console.log(outter()); // 4, 但我想要的是5 console.log(outter()); // 4, 但我想要的是6
會發現每次調用都是4,由於當你調用outter()
的時候,x在最開始都會被從新賦值爲3而後自增,因此每次拿到的值都是固定的,並不會動態增長。那這時該咋辦呢? 這裏閉包就能派上用場了!
還記得以前所說的嗎,內部函數能夠調用外部函數內聲明的變量,咱們先看一下在內部函數操做一番後,咱們可否拿到x的值
// 外部函數 function outter() { let x = 3; // 內部函數 function inner() { // 在內部函數操做x x++; } // 調用一次內部函數,將x進行更新 inner(); // 最後將x進行返回 return x; } console.log(outter()); // 4 console.log(outter()); // 4 console.log(outter()); // 4
這樣是能夠得到x的值,但這樣仍是達不到咱們計數器的要求,由於每次調用outter()
時,x的值都會被從新賦值爲3。 咱們應當繞過outter()
函數從新賦值的步驟,只須要得到x自增的操做就能夠了。 怎麼只獲取自增的操做呢,如今自增的操做是在內部函數inner()
裏,咱們可否只拿到內部函數?固然能夠啦!!
JS是一個弱類型語言,而且支持高級函數。就是說,JS裏函數也能夠做爲一個變量來進行操做!咱們在外部函數outter()
裏將內部函數做爲變量進行返回,就能夠拿到內部函數了 。接下來要仔細理解代碼,這種操做就是閉包:
// 外部函數 function outter() { let x = 3; // 內部函數 function inner() { // 在內部函數裏操做x x++; // 每次調用內部函數的時候,會返回x的值 return x; } // 將inner()函數做爲變量返回,這樣當別人調用outter()時就能夠拿到inner()函數了 return inner; } let fun = outter(); // 此時拿到是函數inner(),就是說fun此時是一個函數 // 咱們接下來調用fun函數(就等於在調用inner函數) console.log(fun()); // 4 console.log(fun()); // 5 console.log(fun()); // 6
能夠看到上面代碼完美拿到了內部函數inner()
,並實現了需求。內部函數對外部函數的變量(環境)進行了操做,而後外部函數將內部函數做爲返回值進行返回,這就是閉包。上面代碼的思路步驟就是:
調用外部函數outter()
---> 拿到內部函數inner()
---> 調用inner()
函數 --- > 成功對outter()
函數內的變量進行了操做。
看到這有人可能會說,我爲啥要多一節步驟,要先拿到內部函數,再對變量進行操做啊?不能直接在外部函數裏對變量進行操做,省了中間兩個步驟嗎? 哥,以前不是演示了嗎,若是直接從外部函數操做,變量值是「死」的,你是沒法實現動態操做變量的。 由於外部函數每次調用完畢後,會銷燬變量,若是再從新調用則會從新爲變量開闢空間並從新賦值。內部函數的話則會將外部函數的變量存放到內存中,從而實現動態操做
上面演示的是內部函數能夠操做外部函數的變量,其實不只僅是某個變量這麼簡單,內部函數能夠操做外部函數所擁有的環境,並能夠攜帶整個外部函數的環境。這句話若是不能理解也徹底不要緊,絲絕不影響你日常使用閉包,使用的多了天然而然就會明瞭。咱們接下來繼續演示閉包,更加加深理解:
如今我有一個需求,我想讓一些函數運行的同時並打印日誌。這個打印日誌的操做並不屬於函數自己的邏輯,須要剝離開來額外實現,這種「擴展功能」的需求就是典型的裝飾模式。咱們先來看一下普通的作法是怎樣的:
function fun() { console.log('fun函數的操做'); } fun(); // fun函數的操做
咱們要對fun()
函數進行擴展功能,最直接的辦法固然是修改fun()
函數的源代碼咯:
function fun() { console.log('額外功能:在運行函數前打印日誌'); console.log('fun函數的操做'); // fun()函數自己的功能 console.log('額外功能:在運行函數後打印日誌'); }
這樣是達到了需求,可是若是我有幾十個函數須要擴展功能呢,豈不是要修改幾十次函數源代碼?上面只是爲了作演示,將擴展功能寫的很簡單隻有兩句代碼,可每每不少擴展功能可不止幾行代碼那麼簡單。何況,不少時候就不容許你修改函數的源代碼!因此上面這種作法,是徹底不行的。
這時候,咱們就能夠用到閉包來實現了。函數能夠當作變量並進行返回,那麼函數天然也能夠當作變量做爲參數進行傳遞。這就很是很是靈活、方便了。我將須要擴展的函數當作參數傳遞進來,而後在個人函數裏進行額外的操做就能夠了:
// 須要被擴展的函數 function fun() { console.log('fun函數的操做'); } // 閉包的外部函數,須要接收一個是函數的參數 function outter(f) { // 此時f就是外部函數的一個成員變量,內部函數理所應當的能夠操做這個變量 function inner() { console.log('額外功能:在運行函數前打印日誌'); f(); // 調用外部函數的變量f,也就是說調用須要被擴展的函數 console.log('額外功能:在運行函數後打印日誌'); } // 外部函數最後將內部函數inner返回出去 return inner; } // 調用外部函數,並傳遞參數進去. 這樣就能夠拿到已經擴展後的函數:inner() let f = outter(fun); f(); // 此時f函數已經將原來的fun()函數功能擴展了,就至關因而inner() // 通常裝飾模式都是將原函數給覆蓋: fun = outter(fun); fun(); // 此時再調用原函數的話,其實就是在調用inner(),是包含了擴展功能的 /* 輸出內容: 額外功能:在運行函數前打印日誌 fun函數的操做 額外功能:在運行函數後打印日誌 */
經過閉包就完美實現了裝飾模式,若是還有其餘函數須要擴展的話,直接調用outter()
函數便可,簡單方便。若是上面這個操做看不明白,千萬不要想複雜了,第一個閉包演示是操做變量x,這個閉包演示也是操做變量,只不過這個變量f
是一個函數罷了。本質沒有任何區別。
如今咱們來對閉包進行總結一下,原理方面就不談了,就只談使用。
使用的思路是:
調用外部函數outter()
---> 拿到內部函數inner()
---> 調用inner()
函數 --- > 成功對outter()
函數內的變量(環境)進行了操做。
閉包是啥呢 ?就是將內部函數做爲返回值返回,內部函數則對外部函數的變量(環境)進行操做。
爲啥要經過內部函數這一步驟呢?由於內部函數能夠將外部函數的環境存放到內存裏,從而實現提供了更爲靈活、方便的操做。
閉包的使用不難,當你使用熟練以後,再去了解背後原理就會很是輕鬆了。
本文只終於講解閉包的基本使用,其餘稍微深一點的東西就不講了,有興趣的能夠去擴展一下: