JS 閉包(closure)

這幾天看到閉包一章,從工具書到各路大神博客,都各自有着不一樣的理解,如下我將選擇性的抄(咳咳,固然仍是會附上本身理解的)一些大神們對閉包的原理及其使用文章,看成是本身初步理解這一功能函數的過程吧。javascript

首先先上連接:html

簡書做者波同窗的JS進階文章系列:

前端基礎進階系列

其餘:

JS祕密花園

javascript深刻理解js閉包

阮一峯《JavaScript標準參考教程》

一不當心就作錯的JS閉包面試題

還有一些也很不錯,但主要是以應用爲主,原理解釋沒有上面幾篇深刻,不過做爲閉包的拓展應用其實也能夠看一看;前端

JavaScript中的匿名函數及函數的閉包java


紅皮書《JS高程》的閉包:

閉包是指有權訪問另外一個函數做用域中的變量的函數。建立閉包的常見方式,就是在一個函數內部建立另外一個函數。

從這句話咱們知道:閉包是一個函數git

function createComparisonFunction(propertyName) {

        return function(obj1,obj2) {
            var value1 = obj1[propertyName];
            var value2 = obj2[propertyName];
            
            if (value1 < value2) {
                return -1;
            } else if (value1 > value2) {
                return 1;
            } else {
                return 0;
            }
        };
    }

這段代碼,咱們能直接看出,共存在三個做用域,Global、createComparisonFunction、匿名函數funciton,因其JS的做用域鏈特性,後者能訪問自身及前者的做用域。而返回的匿名函數即便在其餘地方被調用了,但它仍能夠訪問變量propertyName。之因此還可以訪問這個變量,是由於內部函數的做用域鏈中包含createComparisonFunction的做用域。咱們來深刻了解一下,函數執行時具體發生了什麼?github


當第一個函數被調用時,會建立一個執行環境(Execution Context,也叫執行上下文)及相應的做用域鏈,並把做用域鏈賦值給一個特殊的內部屬性[[Scope]]。而後,使用this、arguments和其餘命名參數的值來初始化函數的活動對象(Activation Object)。但在做用域鏈中,外部函數的活動對象處於第二位,外部函數的外部函數處於第三位,最後是全局執行環境(Global Context)。面試

換一個栗子:chrome

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

/這個函數返回一個函數數組。表面上看,彷佛每一個函數都應該返回本身的索引值,位置0的函數返回0,位置1的函數返回1,以此類推。但但實際上,每一個函數都返回10,爲何?
數組對象內的匿名函數裏的i是引用createFunctions做用域內的,當調用數組內函數的時候,createFunctions函數早已執行完畢。編程

這圖不傳也罷了,畫得忒醜了。
數組內的閉包函數指向的i,存放在createFunctions函數的做用域內,確切的說,是在函數的變量對象裏,for循環每次更新的i值,就是從它那兒來的。因此當調用數組函數時,循環已經完成,i也爲循環後的值,都爲10;數組

有人會問,那result[i]爲何沒有變爲10呢?
要知道,做用域的斷定是看是否在函數內的,result[i] = function.......是在匿名函數外,那它就仍是屬於createFunctions的做用域內,那result[i]裏的i就依然會更新

那麼如何使結果變爲咱們想要的呢?也是經過閉包。

function createFunctions() {
        var result = [];
        
        for (var i=0;i<10;i++) {
            !function(i) {
                result[i] = function() {console.log(i)};
            }(i);
        }
        return result;
    }
    var arr = createFunctions();
    arr[0]();
    arr[1]();
    arr[2]();

function createFunctions() {
        var result = [];
        function fn(i) {
            result[i] = function() {console.log(i)}
        };
        for (var i=0;i<10;i++) {
            fn(i);
        }
        
        return result;
    }
    var arr = createFunctions();
    arr[0]();
    arr[1]();
    arr[2]();

var arr = [];
    function fn(i) {
        arr[i] = function() {console.log(i)}
    }
    function createFunctions() {
        for (var i=0;i<10;i++) {
            fn(i);
        }
    }
    fn(createFunctions());
    arr[0]();
    arr[1]();
    arr[2]();

以第一種爲例,經過一個當即調用函數,將外函數當前循環的i做爲實參傳入,並存放在當即調用函數的變量對象內,此時,這個函數當即調用函數和數組內的匿名函數就至關於一個閉包,數組的匿名函數引用了當即調用函數變量對象內的i。當createFuncions執行完畢,裏面的i值已是10了。可是因爲閉包的特性,每一個函數都有各自的i值對應着。對數組函數而言,至關於產生了10個閉包。

因此能看出,閉包也十分的佔用內存,只要閉包不執行,那麼變量對象就沒法被回收,因此不是特別須要,儘可能不使用閉包。


關於this對象

在閉包中使用this對象也會致使一些問題。咱們知道,this對象是在運行時基於函數的執行環境綁定的;在全局對象中,this等於window,而當函數被做爲某個對象的方法調用時,this等於那個對象。不過,匿名函數的執行環境具備全局性,所以其this對象一般指向window。但有時候因爲編寫閉包的方式不一樣,這一點可能不會那麼明顯。(固然能夠用call和apply)

var name = "The Window";
    
    var obj = {
        name:"My Object",
        getName:function () {
            var bibao = function () {
                return this.name;
            };
            return bibao;
        }
    };
    alert(obj.getName()());            // The Window

先建立一個全局變量name,又建立一個包含name屬性的對象。這個對象包含一個方法——getName(),它返回一個匿名函數,而匿名函數又返回this.name。因爲getName()返回一個函數,所以調用obj.getName()();就會當即調用它返回的函數,結果就是返回一個字符串。然而,這個例子返回的字符串是"The Window",即全局name變量的值。爲何匿名函數沒有取得其波包含做用域(或外部做用域)的this對象呢?

每一個函數調用時其活動對象都會自動取得兩個特殊變量:thisarguments
內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數中的這兩個變量。不過,把外部做用域中的this對象保存在一個閉包可以訪問到的變量裏,就可讓閉包訪問該對象了。

var name = "The Window";
    
    var obj = {
        name:"My Object",
        getName:function () {
            var that = this;
            return function () {
                return that.name;
            };
        }
    };
    alert(obj.getName()());

thisarguments也存在一樣的問題,若是想訪問做用域中arguments對象,必須將該對象的引用保存到另外一個閉包可以訪問的變量中。

var name = "The Window";
    
    var obj = {
        name:"My Object",
        getName:function (arg1,arg2) {
            var arg = [];
            arg[0] = arg1;
            arg[1] = arg2;
            function bibao() {
                return arg[0]+arg[1];
            }
            return bibao;
        }
    };
    alert(obj.getName(1,2)())

obj.getName方法保存了其接收到的實參在它的變量對象上,並在執行函數結束後沒有被回收,由於返回的閉包函數引用着obj.Name方法裏的arg數組對象。使得外部變量成功訪問到了函數內部做用域及其局部變量。

在幾種特殊狀況下,this引用的值可能會意外的改變。

var name = "The Window";        
    var obj = {
        name:"My Object",
        getName:function () {
            return this.name;
        }
    };

這裏的getName()只簡單的返回this.name的值。

var name = "The Window";        
    var obj = {
        name:"My Object",
        getName:function () {
            console.log(this.name);
        }
    };
    obj.getName();                        // "My Object"
    (obj.getName)();                      // "My Object"
    (obj.getName = obj.getName)();    // "The Window"

第一個obj.getName函數做爲obj對象的方法調用,則天然其this引用指向obj對象。
第二個,加括號將函數定義以後,做爲函數表達式執行調用,this引用指向不變。
第三個,括號內先執行了一條賦值語句,而後在調用賦值後的結果。至關於從新定義了函數,this引用的值不能維持,因而返回"The Window"


閉包與setTimeout()

setTimeout結合循環考察閉包是一個很老的面試題了

// 利用閉包,修改下面的代碼,讓循環輸出的結果依次爲1, 2, 3, 4, 5
    for (var i=1; i<=5; i++) { 
        setTimeout( function timer() {
            console.log(i);
        }, i*1000 );
    }

setTimeout的執行與咱們日常的JS代碼執行不同,這裏須要提到一個隊列數據結構執行的概念。

關於setTimeout與循環閉包的思考題

我的理解:因爲setTimeout函數的特殊性,須等其餘非隊列結構代碼執行完畢後,這個setTimeout函數纔會進入隊列執行棧。

用chrome開發者工具分析這段代碼,能夠先本身分析一次,看看依次彈出什麼?

setTimeout(function() {
        console.log(a);
    }, 0);
    
    var a = 10;
    
    console.log(b);
    console.log(fn);
    
    var b = 20;
    
    function fn() {
        setTimeout(function() {
            console.log('setTImeout 10ms.');
        }, 10);
    }
    
    fn.toString = function() {
        return 30;
    }
    
    console.log(fn);
    
    setTimeout(function() {
        console.log('setTimeout 20ms.');
    }, 20);

    fn();

答案:
圖片描述


圖片描述

設置斷點如圖所示,今天剛學Chrome的開發者工具,有哪些使用上的錯誤還請指出。
我分別給變量a、b、fn函數都設置了觀察,變量的值變化將會實時地在右上角中顯示,能夠看到,在JS解釋器運行第一行代碼前,變量a、b就已經存在了,而fn函數已經完成了聲明。接下來咱們繼續執行。要注意:藍色部分說明這些代碼將在下一次操做中執行,而不是已經執行完畢。
圖片描述

把第一個setTimeout函數執行完畢後也沒有反應。我給三個setTimeout內的匿名函數也加上觀察選項,卻顯示不可以使用。
圖片描述

因此,下一次執行會發生什麼?對console出b的值,可是b沒賦值,右上角也看到了,因此顯示undefined。
而console.log(fn)就是將fn函數函數體從控制檯彈出,要注意,console會隱式調用toString方法,這個會在後面講到。
圖片描述
圖片描述

如今第26行以前(不包括26行)的代碼都已略過,a,b變量也已獲得賦值,繼續執行。
重寫了toString方法前:
圖片描述

重寫後:
圖片描述

toString方法是Object全部,全部由它構造的實例都能調用,如今這個方法被改寫並做爲fn對象的屬性(方法)保留下來。
console會隱式調用toString方法,因此30行的console會彈出30;
圖片描述
圖片描述

繼續執行,定義setTimeout函數也是什麼沒有發生,知道調用fn前。
圖片描述

調用fn,是否是就會執行setTimeout函數呢?其實沒有,咱們能夠看到call stack一欄已是fn的執行棧了,可是依舊沒發生什麼。
可是:
圖片描述
圖片描述

當call stack裏的環境都已退出,執行棧裏沒有任何上下文時,三個setTimeout函數就執行了,那這三個時間戳函數那個先執行,那個後執行呢?由設定的延遲時間決定,這個延遲時間是相對於其餘代碼執行完畢的那一刻。
不信咱們能夠經過改變延遲時間從新試一次就知道了。

咱們在看回原來的閉包代碼:

// 利用閉包,修改下面的代碼,讓循環輸出的結果依次爲1, 2, 3, 4, 5
    for (var i=1; i<=5; i++) { 
        setTimeout( function timer() {
            console.log(i);
        }, i*1000 );
    }

先確認一個問題,setTimeout函數裏的匿名函數的i指向哪兒?對,是全局變量裏的i。
setTimeout裏的匿名函數執行前,外部循環已經結束,i值已經更新爲6,這時setTimeout調用匿名函數,裏面的i固然都是6了。

咱們須要建立一個可以保存當前i值的"盒子"給匿名函數,使得匿名函數可以引用新建立的父函數。

// 利用閉包,修改下面的代碼,讓循環輸出的結果依次爲1, 2, 3, 4, 5
    for (var i=1; i<=5; i++) { 
        !function (i) {
            setTimeout( function timer() {
                console.log(i);
            }, i*1000 );
        }(i);
    }

自調用函數就是那個"盒子"


關於《JavaScript編程全解》中的說明

考慮這個函數:

function f(arg) {
        var n = 123 + Number(arg);
        function g() {console.log("n is "+n);console.log("g is called");}
        n++;
        function gg() {console.log("n is "+n);console.log("g is called");}
        
        return [g,gg];
    }

調用數組內函數的console結果是什麼?

var arr = f(1);
    
    arr[0]();            // 對閉包g的調用
    // "n is 125"    "g is called"
    
    
    arr[1]();            // 對閉包gg的調用
    // "n is 125"    "gg is called"

函數g與函數gg保持了各自含有局部變量n的執行環境。因爲聲明函數g時與聲明函數gg時的n值是不一樣的,所以閉包g與閉包gg貌似將會表示各自不一樣的n值。實際上二者都將表示相同的值。由於它們引用了同一個對象。

即都是引用了,f函數執行環境內變量對象內的n值。當執行f(1)的時候,n值就已經更新爲最後計算的值。


防範命名空間的污染

模塊:

在JavaScript中,最外層代碼(函數以外)所寫的名稱(變量名與函數名)具備全局做用域,即所謂的全局變量與全局函數。JavaScript的程序代碼即便在分割爲多個源文件後,也能相互訪問其全局名稱。在JavaScript的規範中不存在所謂的模塊的語言功能。

所以,對於客戶端JavaScript,若是在一個HTML文件中對多個JavaScript文件進行讀取,則他們相互的全局名稱會發生衝突。也就是說,在某個文件中使用的名稱沒法同時在另外一個文件中使用。

即便在獨立開發中這也很不方便,在使用他們開發的庫之類時就更加麻煩了。
此外,全局變量還下降了代碼的可維護性。不過也不能就簡單下定論說問題只是由全局變量形成的。這就如同在Java這種語言規範並不支持全局變量的語言中,一樣能夠很容易建立出和全局變量功能相似的變量。

也就是說,不該該只是一昧地減小全局變量的使用,而應該造成一種儘量避免使用較廣的做用域的意識。對於較廣的做用域,其問題在於修改了某處代碼以後,會難以肯定該修改的影響範圍,所以代碼的可維護性會變差。

避免使用全局變量

從形式上看,在JavaScript中減小全局變量的數量的方法時很簡單的。首先咱們按照下面的代碼這樣預設一下全局函數與全局變量。

// 全局函數
    function sum(a,b) {
        return Number(a)+Number(b);
    }
    // 全局變量
    var position = {x:2,y:3};
    // 藉助經過對象字面量生成對象的屬性,將名稱封入對象的內部。因而從形式上看,全局變量減小了
    var MyModule = {
        sum:function (a,b) {
            return Number(a)+Number(b);
        },
        position:{x:2,y:3}
    };

    alert(MyModule.sum(3,3));        // 6
    alert(MyModule.position.x);        // 2

上面的例子使用對象字面量,不過也能夠像下面這樣不使用對象字面量。

var MyModule = {};            // 也能夠經過new表達式生成
    MyModule.sum = function (a,b) {return Number(a)+Number(b);};
    MyModule.position = {x:2,y:3};

這個例子中,咱們將MyModule稱爲模塊名。若是徹底採用這種方式,對於1個文件來講,只須要一個模塊名就能消減全局變量的數量。固然,模塊名之間仍然可能產生衝突,不過這一問題在其餘程序設計語言中也是一個沒法被避免的問題。
經過這種將名稱封入對象之中的方法,能夠避免名稱衝突的問題。可是這並無解決全局名稱的另外一個問題,也就是做用域過廣的問題。經過MyModule.position.x這樣一個較長的名稱,就能夠從代碼的任意一處訪問該變量。

經過閉包實現信息隱藏

// 在此調用匿名函數
    // 因爲匿名函數的返回值是一個函數,因此變量sum是一個函數
    var sum = (function () {
        // 沒法從函數外部訪問該名稱
        // 實際上,這變成了一個私有變量
        // 通常來講,在函數被調用以後該名稱就沒法再被訪問
        // 不過因爲是在被返回的匿名函數中,因此仍能夠繼續被使用
        var p = {x:2,y:3};
        
        // 一樣是一個從函數外沒法被訪問的私有變量
        // 將其命名爲sum也能夠。不過爲了不混淆,這裏採用其餘名稱
        function sum_internal(a,b) {
            return Number(a)+Number(b);
        }
        // 只不過是爲了使用上面的兩個名稱而隨意設計的返回值
        return function (a,b) {
            alert("x = "+p.x);
            return sum_internal(a,b);
        }
    })();
    console.log(sum(3,4));
    // "x = 2"
    // "y"

上面的代碼能夠抽象爲下面這種形式的代碼。在利用函數做用域封裝名稱,以及閉包可使名稱在函數調用結束後依然存在這兩個特性。這樣信息隱藏得以實現。

(function(){函數體})();

像上面這樣,當場調用函數的代碼看起來或許有些奇怪。通常的作法是先在某處聲明函數,以後在須要時調用。不過這種作法是JavaScript的一種習慣用法,加以掌握。
匿名函數的返回值是一個函數,不過即便返回值不是函數,也一樣能採用這一方法。好比返回一個對象字面量以實現信息隱藏的功能。

var obj = (function() {
        // 從函數外部沒法訪問該名稱
        // 實際上,這是一個私有變量
        var p = {x:2,y:3};
        
        // 這一樣是一個沒法從函數外部訪問的私有函數
        function sum_internal(a,b) {
            return Number(a+b);
        }
        
        // 只不過爲了使用上面的兩個名稱而隨意設計的返回值
        return {
            sum:function (a,b) {
                return sum_internal(a,b);
            },
            x:p.x
        };
    })();
    
    alert(obj.sum(3,4));     // 7
    alert(obj.x);            // 2

閉包與類

利用函數做用域與閉包,能夠實現訪問在控制,上一節中,模塊的函數在被聲明以後當即就對其調用,而是用了閉包的類則可以在生成實例時調用。即使如此,着厚重那個作法在形式上仍然只是單純的函數生命。下面是一個經過閉包來對類進行定義的例子

// 用於生成實例的函數
    function myclass(x,y) {
        return {show:function () {alert(x+" | "+y)}};
    }
    var obj = myclass(3,2);
    obj.show();        // 3 | 2

這裏再舉一個具體的例子,一個實現了計數器功能的類。

這裏重申一下:JavaScript的語言特性沒有"類"的概念。但這裏的類指的是,實際上將會調用構造函數的Function對象。此外在強調對象是經過調用構造函數生成的時候,會將這些被生成的對象稱做對象實例以示區別。

表達式閉包

JavaScript有一種自帶的加強功能,稱爲支持函數型程序設計的表達式閉包(Expression closure)。
從語法結構上看,表達式閉包是函數聲明表達式的一種省略形式。能夠像下面這樣省略只有return的函數聲明表達式中的return{}

var sum = function (a,b) {return Number(a+b)};
    // 能夠省略爲
    var sum = function (a,b) Number(a+b);
相關文章
相關標籤/搜索