JavaScript中的閉包

文章同步到githubhtml

js的閉包概念幾乎是任何面試官都會問的問題,最近把閉包這塊的概念梳理了一下,記錄成如下文章。java

什麼是閉包

我先列出一些官方及經典書籍等書中給出的概念,這些概念雖然表達的不同,可是都在對閉包作了最正確的定義和翻譯,也幫助你們更好的理解閉包,這閱讀起來可能比較模糊,你們日後看,本文經過對多個經典書籍中的例子講解,相信會讓你們能很好的理解js中的閉包。文章開始,我會先鋪墊一下閉包的概念和爲何要引入閉包的概念,而後結合例子來講明講解,並講解如何使用閉包。git

百度百科中的定義:

閉包包含自由(未綁定到特定對象)變量;這些變量不是在這個代碼塊內或者任何全局上下文中定義的,而是在定義代碼塊的環境中定義(局部變量)。「閉包」 一詞來源於如下二者的結合:要執行的代碼塊(因爲自由變量被包含在代碼塊中,這些自由變量以及它們引用的對象沒有被釋放)和爲自由變量提供綁定的計算環境(做用域) -- 百度百科github

《javaScript權威指南》中的概念:

函數對象能夠經過做用域鏈互相關聯起來,函數體內部的變量均可以保存在函數做用域內,這種特性在計算機科學中成爲閉包面試

《javaScript高級教程》中概念:

閉包是指有權訪問另外一個函數做用域中的變量的函數。segmentfault

MDN中的概念

閉包-MDN概念

我的總結的閉包概念:

  1. 閉包就是子函數能夠有權訪問父函數的變量、父函數的父函數的變量、一直到全局變量。歸根結底,就是利用js得詞法(靜態)做用域,即做用域鏈在函數建立的時候就肯定了。
  2. 子函數若是不被銷燬,整條做用域鏈上的變量仍然保存在內存中。

爲何引入閉包的概念

我引入《深刻理解JavaScript系列:閉包(Closures)》文章中的例子來講明,也能夠直接去看那篇文章,我結合其餘書籍反覆讀了不少遍此文章才理解清楚。以下:瀏覽器

function testFn() {

  var localVar = 10;  // 自由變量

  function innerFn(innerParam) {
    alert(innerParam + localVar);
  }

  return innerFn;
}

var someFn = testFn();
someFn(20); // 30

通常來講,在函數執行完畢以後,局部變量對象即被銷燬,因此innerFn是不可能以返回值形式返回的,innerFn函數做爲局部變量應該被銷燬纔對。閉包

這是當函數以返回值時的問題,另外再看一個當函數以參數形式使用時的問題,仍是直接引用《深刻理解JavaScript系列》中的例子,也方便你們有興趣能夠直接去閱讀那篇文章函數

var z = 10;

function foo() {
  alert(z);
}

foo(); // 10 – 使用靜態和動態做用域的時候

(function () {

  var z = 20;
  foo(); // 10 – 使用靜態做用域, 20 – 使用動態做用域

})();

// 將foo做爲參數的時候是同樣的
(function (funArg) {

  var z = 30;
  funArg(); // 10 – 靜態做用域, 30 – 動態做用域

})(foo);

當函數foo在不一樣的函數中調用,z該取哪一個上下文中的值呢,這就又是一個問題,因此就引入了閉包的概念,也能夠理解爲定義了一種規則。學習

理解閉包

函數以返回值返回

看一個《javsScript權威指南》中的一個例子,我稍微作一下修改以下:

var scope = 'global scope';
function checkScope() {
    var scope = 'local scope';
    return function() {
        console.log(scope);
    }
}

var result = checkScope(); 
result();   // local scope checkScope變量對象中的scope,非全局變量scope

分析:

即便匿名函數是在checkScope函數外調用,也沒有使用全局變量scope,便是利用了js的靜態做用域,當匿名函數初始化時,就建立了本身的做用域鏈(做用域鏈的概念這裏不作解釋,能夠參考個人另外一篇文章js中的執行棧、執行環境(上下文)、做用域、做用域鏈、活動對象、變量對象的概念總結,其實當把做用域鏈理解好了以後,閉包也就理解了), 此匿名函數的做用域鏈包括checkScope的活動對象和全局變量對象, 當checkScope函數執行完畢後,checkScope的活動對象並不會被銷燬,由於匿名函數的做用域鏈還在引用checkScope的活動對象,也就是checkScope的執行環境被銷燬,可是其活動對象沒有被銷燬,留存在堆內存中,直到匿名函數銷燬後,checkScope的活動對象纔會銷燬,解除對匿名函數的引用將其設置爲null便可,垃圾回收將會將其清除,另外當外部對checkScope的自由變量存在引用的時候,其活動對象也不會被銷燬

result = null; //解除對匿名函數的引用

註釋:

自由變量是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量

補充:
引用一下《javsScript權威指南》中的補充,幫助你們進一步理解
閉包JavaScript權威指南概念

函數以參數形式使用

當函數以參數形式使用時通常用於利用閉包特性解決實際問題,好比瀏覽器中內置的方法等,下面我直接引用《深刻理解JavaScript系列:閉包(Closures)》中關於閉包實戰部分的例子以下:

sort

在sort的內置方法中,函數以參數形式傳入回調函數,在sort的實現中調用:

[1, 2, 3].sort(function (a, b) {
  ... // 排序條件
});

map

和sort的實現同樣

[1, 2, 3].map(function (element) {
  return element * 2;
}); // [2, 4, 6]

另外利用自執行匿名函數建立的閉包

var foo = {};

// 初始化
(function (object) {

  var x = 10;

  object.getX = function() {
    return x;
  };

})(foo);

alert(foo.getX()); // 得到閉包 "x" – 10

利用閉包實現私有屬性的存取

先來看一個例子

var fnBox = [];
function foo() {
    for(var i = 0; i < 3; i++) {
        fnBox[i] = function() {
            return i;
        }
    }
}

foo();
var fn0 = fnBox[0];
var fn1 = fnBox[1];
var fn2 = fnBox[2];
console.log(fn0()); //  3
console.log(fn1()); //  3
console.log(fn2()); //  3

用僞代碼來講明以下:

fn0.[[scope]]= {
    // 其餘變量對象,一直到全局變量對象
    父級上下文中的活動對象AO: [data: [...], i: 3]
}

fn1.[[scope]]= {
    // 其餘變量對象,一直到全局變量對象
    父級上下文中的活動對象AO: [data: [...], i: 3]
}

fn2.[[scope]]= {
    // 其餘變量對象,一直到全局變量對象
    父級上下文中的活動對象AO: [data: [...], i: 3],
}

分析:

這是由於fn0、fn一、fn2的做用域鏈共享foo的活動對象, 並且js沒有塊級做用域,當函數foo執行完畢的時候foo的活動對象中i的值已經變爲3,當fn0、fn一、fn2執行的時候,其最頂層的做用域沒有i變量,就沿着做用域鏈查找foo的活動對象中的i,因此i都爲3。

可是這種結果每每不是咱們想要的,這時就能夠利用認爲建立一個閉包來解決這個問題,以下:

var fnBox = [];
function foo() {
    for(var i = 0; i < 3; i++) {
        fnBox[i] = (function(num) {
            return function() {
                return num;
            }
        })(i);
    }
}
foo();
var fn0 = fnBox[0];
var fn1 = fnBox[1];
var fn2 = fnBox[2];
console.log(fn0()); //  0
console.log(fn1()); //  1
console.log(fn2()); //  2

用僞代碼來講明以下:

fn0.[[scope]]= {
    // 其餘變量對象,一直到全局變量對象
    父級上下文中的活動對象AO: [data: [...], i: 3],
    fn0自己的活動對象AO: {num: 0} 
}

fn1.[[scope]]= {
    // 其餘變量對象,一直到全局變量對象
    父級上下文中的活動對象AO: [data: [...], i: 3],
    fn1自己的活動對象AO: {num: 1} 
}

fn2.[[scope]]= {
    // 其餘變量對象,一直到全局變量對象
    父級上下文中的活動對象AO: [data: [...], i: 3],
    fn2自己的活動對象AO: {num: 2} 
}

分析:

當使用自執行匿名函數建立閉包, 傳入i的值賦值給num,因爲做用域鏈是在函數初始化時建立的,因此當每次循環時,函數fn十、fn一、fn2的做用域鏈中保存了當次循環是num的值, 當fn十、fn一、fn2調用時,是按照自己的做用域鏈進行查找。

閉包引發的內存泄漏

閉包-內存泄漏

總結

從理論的角度將,因爲js做用域鏈的特性,js中全部函數都是閉包,可是從應用的角度來講,只有當函數以返回值返回、或者當函數以參數形式使用、或者當函數中自由變量在函數外被引用時,才能成爲明確意義上的閉包。

最後,我想表達的式,本篇大量引用和羅列了經典的犀牛書《javaScript權威指南》、紅寶書《javaScript高級教程》、以及《深刻理解JavaScript系列:閉包(Closures)》系列文章中的概念和例子,不爲能造成本身的獨特看法,只爲了能把閉包清晰的講解出來。筆者是個小菜鳥,能力實在有限,也在學習中,但願你們多多指點,如發現錯誤,請多多指正。也但願看過此文的朋友能對閉包多一些理解,那我寫這篇文章也就值得了。下次面試時就能夠告訴面試官什麼是閉包了。謝謝。

相關文章
相關標籤/搜索