JavaScript 來好好盤一盤閉包!

2020-10-21 21:00:55

今天欄目好好介紹閉包。

前言

想要深入學習JavaScript這門語言,閉包這個概念幾乎是繞不開的關鍵,今天就讓我們一起好好地盤一盤,閉包到底是什麼東西。
如果是零基礎的小夥伴,可以先看看前一篇文章,幫助你更好的理解本文的內容:

  • 【JavaScript】有趣的作用域和提升

瞭解一下閉包

1. 什麼是閉包

我們先來看看閉包在MDN中的定義:

一個函數和對其周圍狀態(lexical environment,詞法環境)的參照捆綁在一起(或者說函數被參照包圍),這樣的組合就是閉包(closure)

閉包的定義非常晦澀難懂,如果通俗的解釋一下的話,就是:

  • 一個定義在函數內部的函數和所在作用域的組合稱為閉包。

這麼講可能還是非常抽象,不過我們先有個概念,下面會詳細解釋這個概念。對於閉包,其實還有另一種解釋:

  • 一個函數中宣告了一個內部函數並返回,外部可以通過這個內部函數存取到內部的值,這樣的函數,我們稱作閉包

區別於第一種解釋,第二種解釋把函數本身稱作為閉包,這樣的解釋也合理,但更準確的話其實我們可以把這個函數稱作閉包函數。這樣的話,其實兩種解釋都沒有問題,也可以同時存在了。

2. 感受一下閉包

看完了閉包的概念,我們通過實際的例子感受一下,閉包長什麼樣?閉包產生的條件?

閉包長什麼樣?

function outer(){    var a=0;    function inner(){        console.log(++a);
    }    return inner;
}var f=outer();
f();  //1f();  //2複製程式碼

我們先來看下這個例子裡,閉包是如何體現的。我們首先定義了outer函數,然後在outer函數的內部宣告了一個inner函數並返回。

在外部,我們執行outer函數,並將返回的inner函數儲存在了f中,現在的f就是我們說的閉包函數了。

在例子中,我們執行了兩次f函數,分別輸出了12,說明我們在執行f函數中的console.log(++a);語句時,根據我們之前對作用域的解釋,會去函數所在的作用域鏈上尋找變數,最終在outer函數中找到了這個變數,並進行了增值操作。

同時需要注意的是,我們輸出的結果並不是兩次1,說明我們的作用域是共用的,可以看成outer的作用域延伸到了外部。

outer作用域本來應該在執行結束後釋放,但根據GC機制(我們這裡先不展開介紹)會發現:

  • 我準備釋放outer了,但a變數好像被參照了,而且外部還儲存了起來不知道什麼時候呼叫?好叭,那我就先不釋放了。

閉包就這樣誕生了,在作用域消失之後,外部依然可以存取作用域中的變數

閉包產生的條件?

我們根據剛剛的例子和解釋總結一下,產生一個閉包需要哪些條件:

  1. 在外部函數中宣告一個內部函數,並返回內部函數。
    這裡的outer就是外部函數,inner就是返回的內部函數。
  2. 內部的函數參照了外部函數中的變數。
    我們的例子中,inner函數就參照了變數a
  3. 外部函數需要被執行,建立一個閉包,同時返回的內部函數需要被儲存起來。
    外部函數outer需要被執行,同時需要把返回的內部函數給儲存起來,也就是我們例子中的var f=outer()

只有同時滿足了這三個條件,才能產生閉包。看似很難達成,事實上,我們實際程式碼中經常不經意間就能產生閉包。

通過剛剛的介紹,相信大家應該對閉包有了一個概念,接下來就讓我們出發,深入的瞭解一下閉包的方方面面。

深入體驗一下閉包

1.建立多個閉包

在剛剛的例子中,我們建立了一個閉包,執行多次閉包函數後,增加的都是同一個作用域中的變數a,那麼我們試試建立多個閉包會怎麼樣:

function outer() {  var a = 0;  function inner() {    console.log(++a);
  }  return inner;
}var f1 = outer();var f2 = outer();
f1();  //1f2();  //1複製程式碼

這段程式碼在剛剛的例子上進行了改動,我們執行了兩次外部函數outer,並分別用不同的變數f1f2儲存。當執行f1f2時會發現,輸出的結果都是1,說明f1f2的作用域是獨立的,f1f2屬於兩個不同的閉包,我們用一張圖來理解下:

當分別建立f1f2時,呼叫了兩次outer函數,建立了兩個不同的上下文。而當f1f2分別執行時,根據作用域的查詢規則,會去對應的作用域中查詢變數,並執行增值輸出,所以最終兩個值均為2

2.使用閉包實現模組化

我們知道,作用域的外部無法拿到作用域內部的值,而通過閉包,我們可以把作用域我們需要的值或者方法暴露出去,比如:

function outer() {  var myNumber = 0;  function addNumber() {
    myNumber++;
  }  function getNumber() {    return myNumber;
  }  return {
    addNumber, getNumber
  }
}var module = outer();module.addNumber();module.addNumber();module.getNumber();複製程式碼

在這個例子中,我們同樣定義了一個外部函數outer,另外還分別定義了兩個函數addNumbergetNumber,用於增加和獲取變數myNumber

當我們執行outer()語句的時候,會建立一個上下文,同時把內部返回的物件儲存在module變數上,此時我們就建立了一個閉包,以及一個包含方法addNumbergetNumber的物件。

由於外部是無法直接存取變數myNumber的,但由於閉包的原因,addNumbergetNumber是可以存取到這個變數的,因此我們成功的把變數myNumber隱藏了起來,並且對外只提供了增加和獲取myNumber值的方法。

試著用閉包解決問題

通過剛剛的例子,相信大家應該對閉包有了一定了解,接下來我們試著運用閉包來解決實際問題,先看一下例子:

for (var i = 0; i < 2; i++) {      setTimeout(() => {        console.log(i);
      }, 500);
    }複製程式碼

這是一個十分容易令人誤解的例子。接觸過的小夥伴肯定都知道,最後會輸出兩次2而不是依次輸出01,我們來看看為什麼會這樣。
首先,外部是一個for迴圈,執行了兩次語句。

for (var i = 0; i < 2; i++) {
    ... //  執行兩次}複製程式碼

在函數的內部,我們呼叫了setTimeout函數,關鍵的地方來了,這個函數是一個非同步函數,並不會馬上執行,所以實際上等外部的for迴圈執行結束了,才會真的執行setTimeout中的函數。還有第二個關鍵點在於,for迴圈中,var定義的變數相當於定義在全域性,而不存在塊級作用域。那麼剛剛的程式碼就可以近似的看成這樣了。

var i=0;
i++;    //i=1i++;    //i=2console.log(i);console.log(i);複製程式碼

非常直接粗暴,但可以很清晰的看出輸出結果為何是兩次2了,因為大家共用了同一個作用域,i的值被覆蓋了。那麼知道了問題出在哪裡,我們試著用上我們剛剛學習的閉包,來建立不同的作用域:

   for (var i = 0; i < 2; i++) {      function outer() {        var j = i;        setTimeout(function inner() {          console.log(j);
        }, 500);
      }
      outer();
    }複製程式碼

我們按照閉包的樣式對剛剛的程式碼進行了改造,這裡的setTimeout並不直接就是inner函數,這是因為它在這裡起到了定義inner函數,並儲存執行inner函數的功能。

我們可以看到,最終結果依次輸出了01,說明我們的閉包是成功了的,但這樣的閉包比較臃腫,我們試著提高一下,寫的更加優雅一點:

   for (var i = 0; i < 2; i++) {
        (function() {   //用自執行函數代替了`outer`函數的定義和執行兩個步驟
            var j = i;            setTimeout(function inner() {                console.log(j);
            }, 500);
        })();
    }複製程式碼

還可以再簡化一下:

for (var i = 0; i < 5; i++) {        for (var i = 0; i < 2; i++) {
            (function(j) {//用自執行函數代替了`outer`函數的定義和執行兩個步驟
                setTimeout(function inner() {                    console.log(j);
                }, 500);
            })(i);//var j=i的步驟,通過傳入i值替換
        }
    }複製程式碼

這樣就大功告成了!

總結

本篇首先介紹了閉包的定義以及不同人對閉包的理解,之後介紹了閉包產生的原因並總結了三個條件,之後舉例說明了如何建立多個閉包和通過閉包實現模組,最後講述瞭如何通過閉包解決for迴圈中使用非同步函數依次輸出值的問題。其實閉包沒有想象的那麼可怕,只要你願意靜下心去探索去了解,閉包也會對你敞開大門~

寫在最後

都看到這裡了,如果覺得對你有幫助的話不妨點個贊支援一下唄~

以後會陸續更新更多文章和知識點,感興趣的話可以關注一波~

如果哪裡有錯誤的地方或者描述不準確的地方,也歡迎大家指出交流~

相關免費學習推薦:(視訊)

以上就是JavaScript 來好好盤一盤閉包!的詳細內容,更多請關注TW511.COM其它相關文章!