淺談對JavaScript閉包的理解

在談閉包以前,咱們首先要了解幾個概念:ajax

  • 什麼是函數表達式? 與函數聲明有何不一樣?數組

  • JavaScript查找標識符的機制閉包

  • JavaScript的做用域是詞法做用域app

  • JavaScript的垃圾回收機制函數

先來講說函數表達式設計

什麼是函數表達式? 若是function是聲明中的第一個詞,那麼就是函數聲明,不然就是函數表達式
舉個例子:指針

var foo = function(){}; //匿名函數表達式

(function foo(){})() //函數表達式,由於function不是聲明中的第一個詞,前面還有一個「(」

function foo(){} //函數聲明

函數表達式也分匿名函數表達式和具名函數表達式:code

var foo = function(){} //匿名函數表達式

var foo = function bar(){} //具名函數表達式

具名函數表達式要注意一點:上例中的bar標識符 只在當前的函數做用域中存在,在全局做用域中是不存在的component

函數聲明與函數表達式的重要區別有:對象

  • 函數聲明具備函數聲明提高,函數表達式不會被提高

  • 函數表達式能夠在表達式後跟個括號來當即執行,函數聲明不行

(function (){})() //匿名函數表達式,且當即執行

這種模式的函數,一般稱爲IIFE(Immediately Invoked Function Expresstion)表明當即執行函數表達式。
關於函數、變量聲明的提高這裏就再也不多說了, 想了解的同窗能夠查閱一下相關資料

關於JavaScript執行函數時查找標識符的機制

不瞭解做用域鏈及變量對象的同窗能夠先查閱相關資料後再來看。

做用域鏈本質上是一個由指向變量對象的指針列表,它只引用但不實際包含變量對象,變量,函數等等都存在各自做用域的變量對象中,經過訪問變量對象來訪問它們。

只有在函數調用的時候,纔會建立執行環境和做用域鏈,同時每一個環境都只能逐級向上搜索做用域鏈,來查詢變量和函數名等標識符

JavaScript的做用域

JavaScript的做用域就是詞法做用域而不是動態做用域
詞法做用域最重要的特徵是它的定義過程發生在代碼的書寫階段
動態做用域的做用域鏈是基於調用棧的 詞法做用域的做用域鏈是基於代碼中的做用域嵌套

function foo(){
    console.log(num)
}
   
function bar(){
    var num = 2;
    foo(); // 1
}
    
var num = 1;
bar();

bar函數執行時,會執行foo函數,由於JavaScript是詞法做用域,因此函數執行時,會沿着定義時的做用域鏈查找變量,而不是執行時,foo函數定義在全局中,因此查找到了全局的num,輸出了1而不是2。

下面來講閉包

關於什麼是閉包,其實有不少種說法,這取決於各自的理解,最主要的有兩種:

  • Nicolas C.Zakas:閉包是指有權訪問另外一個函數做用域中的變量的函數

  • KYLE SIMPSON:當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,這個函數持有對該詞法做用域的引用,這個引用就叫作閉包

我我的更傾向於後者對於閉包的定義,即閉包是一個引用。
下面來看一些代碼:

function foo() {
    var a = 5;
    return function() {
    console.log(a);
    }
 }

var bar = foo();
bar();       // 5

這段代碼裏 foo執行時會返回一個匿名函數表達式,這個函數可以訪問foo()的做用域,而且引用能引用它,而後將這個匿名函數賦值給了變量bar,讓bar能引用這個匿名函數而且能夠調用它。
這個例子,匿名函數在本身定義的詞法做用域之外的地方成功執行
這正是閉包強大的地方,好比經過閉包實現模塊模式:

function aModule() {

    var sometext = "module";
    
    function doSomething() {
        console.log(sometext);
    }
    
    return {
        doSomething: doSomething
        };
}

var obj = aModule();
obj.doSomething()   //module

咱們經過調用aModule函數建立了一個模塊實例,函數返回的這個對象,實質上能夠看作是這個模塊的公告API,是否是有些像其它面嚮對象語言中的class?

再來經過閉包實現一個單例模式:

var application = function() {
    
    var components = [];
    /*
    一些初始化操做
    */
    return {              //公共API
        getComponentCount: function() {
        return components.length;
        },
        registerComponent: function(component) {
        components.push(component);
        }
    };
}();

這個例子經過IIFE建立了一個單例對象,函數裏返回的對象字面量是這個單例模式的公共接口。
經過閉包實現模塊模式,能夠作到不少強大的事情,模塊模式能成功實現,最關鍵的是返回的API還能繼續引用定義時所在的做用域,從而進行一些操做,也就是說,做用域並無由於函數執行後被銷燬,也就是沒有被內存回收,之因此沒有被回收是由於閉包的存在和JavaScript的垃圾回收機制。

JavaScript的垃圾回收機制

JavaScript最經常使用的垃圾收集方式是標記清除,垃圾收集器會給存儲在內存中的全部變量都加上標記,而後去除環境中的變量,以及被環境中的變量引用的變量的標記,說明這些變量還有做用,暫時不能被刪除,而後在此以後被加上標記的變量就是要刪除的變量了,等待垃圾收集器對他們完成清除工做。

對函數來講,函數執行完畢後,會自動釋放掉裏面的變量,但是若是函數內部存在閉包,它們就不會被刪除,由於這個函數還在被內部的函數所引用,因此他不會被加上標記,不會被清除,而是會一直存在內存中得不到釋放!除非使用閉包的那個內部函數被銷燬,外部函數才能獲得釋放

因此,雖然閉包強大,可是咱們不能濫用它,且在沒有必要的狀況下儘可能不要建立閉包,否則將會有大量的變量對象得不到釋放,過分佔用內存。

關於循環和閉包

當循環和閉包結合在一塊兒時,常常會產生讓初學者以爲匪夷所思的問題。
來看一段Nicolas C.Zakas 在《JavaScript高級程序設計》中的代碼:

function createFunction() {
    var result = [];
    for (var i = 0; i < 10; i++) {
        result[i] = function() {
            return i;
        };
    }
    return result;
}

這個函數執行後,會建立一個由十個函數組成的數組而且產生十個互不相干的函數做用域,表面上看調用第幾個函數就會輸出幾,可是結果並非這樣

var result = createFunction();
result[0]();  // 10
result[9]();  // 10

產生這種奇怪的現象的緣由就是以前說的,createFunction的變量對象由於閉包的存在沒有被釋放,注意閉包保存的是整個變量對象,而不是隻保存只被引用的變量,在createFunction執行後,建立了十個函數,同時變量 i 沒有被釋放,依然保存在內存中,因此此時它的值保留爲中止循環後的10。

當咱們在外部調用函數時,函數沿着它的做用域鏈開始搜索所須要的變量,前面說過,JavaScript的做用域鏈是基於定義時的做用域嵌套,因此當咱們調用某個函數好比 result[0] 它就會首先在本身的做用域裏經過RSH搜索 i ,顯然 i 不存在這個做用域中,因而它又沿着做用域鏈向上一級做用域中搜索 i ,而後找到了 i ,可是此時createFunction函數已經執行,循環也已經執行完畢了, i 的值爲10,因此獲取到的i,值就爲10,同理,其餘的函數執行時,查找的i 也會是10, 因此每一個函數執行結果都是輸出10。
關鍵所在就是儘管循環中的十個函數是在各自的迭代中分別定義的,可是它們都處於一個共享的上一級做用域中,因此它們獲取到的都是一個 i

因此解決此類問題的關鍵就是讓函數查找i時,不找到createFunction的變量對象那一級 ,由於一旦向上搜索到createFunction那裏,獲得的就是10。因此咱們能夠經過一些方法在中間來截斷本該搜索到createFunction變量對象的一次查找。

首先咱們能夠這樣:

function createFunction() {
    var result = [];
    for (var i = 0; i < 10; i++) {
    (function (){
        result[i] = function() {
            return i;
        };})();
    }
    return result;
}

咱們經過定義一個當即執行函數表達式,在result[i]函數上一級建立了一個塊級做用域,若是咱們把這個塊級做用域叫作a,那麼它查找i時是這樣一條鏈 result[i]->a->createFunction,之因此還會查找到createFunction中,是由於a中沒有i這個變量,因此咱們須要作些什麼,讓它搜索到a時就停下

function createFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        (function(i){
        result[i] = function() {
            return i;
        };})(i);
        }
    
    return result;
}

如今a這個塊級做用域裏定義了一個變量 i ,這個 i 與上級的 i 不會互相影響,由於它們存在各自的做用域裏, 同時咱們將該次迭代時的 i 值賦給了 a這個塊級做用域裏的 i ,即a中的 i 保存了當次迭代的 i ,result[i]在外部執行時,是這樣的調用鏈result i -> a在a中就能找到須要的變量,不須要再向上搜索,也不會查找到值爲10的 i ,因此調用哪一個result[i]函數,就會輸出哪一個 i 。

ES6 中咱們還可使用 let 來解決此類問題

function createFunction() {
    var result = [];
    for (var i = 0; i < 10; i++) {
        let j = i;
        result[i] = function() {
            return j;
        };
    }
    return result;
}
//輸出一下
console.log(createFunction()[2]());  //2

let會建立一個塊級做用域,並在這個做用域中聲明一個變量。因此咱們至關於在result[i]上套了一層塊級做用域

function createFunction() {
    var result = [];
    for (var i = 0; i < 10; i++) {
        //塊的開始
        let j = i;
        result[i] = function() {
            return j;
        };
        //塊的結束
    }
    return result;
}

這種方式解決此類問題,與前面沒有多大分別,總之就是爲了避免讓函數調用時去查找到最上級的那個 i 。

其實,若是在for循環頭部來進行let聲明還會有一個有趣的行爲:

function createFunction() {
    var result = [];
    for (let i = 0; i < 10; i++) {    //每次迭代,都會聲明一次i,總共聲明10次
        result[i] = function() {
            return i;
        };
    }
    return result;
}
console.log(createFunction()[2]());  //2

這樣在for頭部使用let聲明, 每次迭代都會進行聲明,隨後每次迭代都會使用上一個迭代結束時的值來初始化這個變量。

事實上當函數當作值類型並處處傳遞時, 基本都會使用閉包,如定時器,跨窗口通訊,事件監聽,ajax等等 基本只要使用了回調函數, 實際上就是在使用閉包。

閉包是一把雙刃劍 是JavaScript比較難以理解和掌握的部分, 它十分強大,卻也有很大的缺陷,如何使用它徹底取決於你本身。

以上皆爲我的觀點 如如有誤 還望指正

參考書籍

  1. 《JavaScript高級程序設計》

  2. 《你不知道的JavaScript 上卷》

相關文章
相關標籤/搜索