輕鬆瞭解JS中,閉包的基本使用和裝飾模式的實現

@[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()函數內的變量(環境)進行了操做。

閉包是啥呢 ?就是將內部函數做爲返回值返回,內部函數則對外部函數的變量(環境)進行操做。

爲啥要經過內部函數這一步驟呢?由於內部函數能夠將外部函數的環境存放到內存裏,從而實現提供了更爲靈活、方便的操做。

閉包的使用不難,當你使用熟練以後,再去了解背後原理就會很是輕鬆了。

小擴展

本文只終於講解閉包的基本使用,其餘稍微深一點的東西就不講了,有興趣的能夠去擴展一下:

  1. 在面向對象(OOP)的設計模式中,好比Java,裝飾模式是須要經過繼承和組合來實現,裝飾者和被裝飾者必須都繼承了同一個抽象組件。 而JS中,則經過閉包很是靈活的實現了裝飾模式,任何函數均可以被裝飾從而擴展功能。不過這還不算最方便,在python裏直接是從語法層面提供了裝飾模式,即裝飾器。 JS在ES6也經過語法層面實現了裝飾器,不過和python的有些不太同樣,有興趣的能夠去了解一下。
  2. 內部函數能夠操做外部函數的變量,上面樣式的那些變量都是固定的值,若是變量是一個引用的值(好比引用了外面的一個數組,在外部函數的外面也能夠直接對數組進行修改),會產生什麼後果。
  3. 運用閉包的好處上面已經演示了,那閉包的壞處是什麼?提示:內存
相關文章
相關標籤/搜索