閉包的概念與應用

什麼是閉包?

做爲一個 JavaScript 語言的開發者,提起閉包確定不會感到陌生,那麼到底什麼纔是閉包哪?javascript

閉包不是什麼新奇的概念,它早在高級語言開始發展的年代就產生了。閉包(Closure)是詞法閉包的簡稱。對閉包的具體定義有不少種說法,這些說法大致能夠分爲兩類:java

  • 一種說法認爲閉包是符合必定條件的函數。認爲閉包是在其詞法上下文中引用了自由變量(自由變量是指局部變量之外的變量)的函數。
  • 另外一種說法認爲閉包是函數和與其相關的引用環境組合而成的實體。認爲閉包是在實現深約束時,須要建立一個能顯示錶示引用環境的東西,並將它與相關的子程序捆綁在一塊兒,這樣捆綁起來的總體被稱爲閉包。

這兩種定義在某種意義上是對立的,一個認爲閉包是函數,另外一個認爲閉包是函數和引用環境組成的總體。很明顯第二種說法更確切一些,閉包只是在形式和表現上像函數,但實際上不是函數。函數是一些可執行的代碼,這些代碼在函數被定義後就肯定了,不會在執行時發生變化,因此一個函數只有一個實例。閉包在運行時能夠有多個實例,不一樣的引用環境和相同的函數組合能夠產生不一樣的實例。所謂引用環境是指在程序執行中的某個點全部處於活躍狀態的約束組成的集合。其中的約束是指一個變量的名字和其所表明的對象之間的聯繫。linux

JavaScript 閉包的本質

在支持嵌套做用域的語言中,有時不能簡單直接的肯定函數的引用環境。這樣的語言通常具備這樣的特性:設計模式

  • 函數是一等公民,即函數能夠做爲一個函數的返回值或參數,還能夠做爲一個變量的值
  • 函數能夠嵌套定義,即在一個函數內部能夠定義另外一個函數。

JavaScript 閉包的源自兩點,詞法做用域和函數當作值傳遞數組

做用域是查找變量時的一些規則。詞法做用域就是定義在詞法階段的做用域。或者換句話說,詞法做用域是由你書寫代碼時將變量和塊做用域寫在哪裏來決定的。按照代碼書寫時的樣子,內部函數能夠順着做用域鏈一層一層地查找、訪問函數外的變量,或者咱們叫它自由變量。瀏覽器

函數當作值傳遞,也就是上面所說的函數是一等公民。函數內部的自由變量是在外層函數執行時建立的,外層函數執行完之後,這些變量理應被銷燬,可是若是將內層函數做爲返回值返回,這些自由變量就被保存了下來。並且沒法訪問,必須經過內層函數來訪問。原本執行過程和詞法做用域是封閉的,將內層函數做爲返回值返回就提供了一種訪問自由變量的方式。閉包

一個函數如何能封閉外部狀態哪?當外部狀態的scope失效的時候,還有一份留在內部狀態裏面。在執行過程當中,返回函數,或者將函數得以保留下來,而且函數中有自由變量就會造成閉包。一個函數中沒有自由變量時,引用環境不會發生變化函數

閉包的應用

知道了什麼是閉包,也理解了閉包的本質,下面能夠了解下閉包的幾種應用,或許你在平常的開發中已經用到很多了。ui

封裝私有變量,存儲計算的值

// 將計算的結果保存在 sum 中
function add(init) {
  var sum = init;
  return function getSum(number) {
    sum += number;
    return sum;
  }
}
複製代碼

延遲計算

// 延遲計算
function add(init) {
  var sum = init;
  var args = [];
  return function getSum() {
    // 當參數到達必定的數量時再進行運算
    args = args.concat(Array.from(arguments));
    if(args.length > 5) {
      for(let i = 0; i < args.length; i++) {
        sum += args[i];
      }
      return sum;
    }
  }
}
複製代碼

延續局部變量的壽命

img 對象常常用於進行數據上報,可是經過查詢後臺的記錄能夠得知,由於一些低版本的瀏覽器的實現可能存在 bug,在這些瀏覽器下使用 report 函數進行數據上報會丟失 30% 左右的數據,也就是說,report 函數並非每一次都發起了 HTTP 請求。丟失數據的緣由是 img 是 report 函數中的局部變量,當 report 函數調用結束後, img 局部變量隨即被銷燬,而此時或許還沒來得及發出 HTTP 請求,因此這次請求就會丟失掉。spa

// 這種方法會丟失 30% 左右的數據
var report = function (src) {
  var img = new Image();
  img.src = src;
};

// 把 img 變量封裝起來,就能夠解決請求丟失的問題
var report = (function(){
  var imgs = [];
  return function(src) {
    var img = new Image();
    imgs.push(img);
    img.src = src;
  }
})();
複製代碼

私有數據和應用程序接口

有時,你想強制程序與數據的交互方式,以便保護其完整性。經過是使用閉包,徹底能夠作到這一點。建立此類接口的一種常見方法就是從函數返回對象。這時,定義在原函數中的數據只能由返回對象上定義的方法訪問,下面是一個例子:

function makeCalendar(name) {
  var calendar = {
    owner: name,
    events: [],
  };
  
  return {
    addEvent: function(event, dateString) {
      var eventInfo = {
        event: event,
        date: new Date(dateString),
      };
      calendar.events.push(eventInfo);
      calendar.events.sort(function(a, b) {
        return a.date - b.date;
      });
    },
    
    listEvents: function() {
      if (calendar.events.length > 0) {
        console.log(calendar.owner + "'s events are: ");
        
        calendar.events.forEach(function(eventInfo) {
          var dateStr = eventInfo.date.toLocaleDateString();
          var description = dateStr + ": " + eventInfo.event;
          
          console.log(description);
        });
      } else {
        console.log(calendar.owner + " has no events.");
      }
    },
  };
}
複製代碼

閉包與內存管理

局部變量原本應該在函數退出的時候被解除引用,但若是局部變量被封閉在閉包造成的環境中,那麼這個局部變量就會一直存在。在這個意義上看,閉包的確會使一些數據沒法被及時銷燬。使用閉包的一部分緣由是咱們選擇主動把一些變量封閉在閉包中,由於可能在之後還須要使用這些變量,把這些變量放在閉包中和放在全局做用域,對內存方面的影響是一致的。若是在未來須要回收這些變量的時候,能夠手動把這些變量設置爲 null。

參考內容

相關文章
相關標籤/搜索