閉包及其應用

輝衛無敵發表於2018-07-05

概念

javascript高階程式設計對它的定義是:能夠獲取其他函式作用域內變數的函式。換句話說就是定義在其他函式內部的函式。

我們來看一個經典的閉包例子,也是面試時經常會遇到的。

function createFunction() {
    var result = new Array();

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

    return result;
}
var resultFun = createFunction();
複製程式碼

面試官會問你,resultFun[6]()的結果是神馬?大家都知道的,是10。面試官會接著問你,為什麼是10,答是閉包。為什麼閉包會造成這種結果,....;可能有的小夥伴就會懵逼了。下面我們就來解釋一下為什麼會這樣。

首先我們想一下,resultFun[6]這個函式中的變數i,它是指向誰的。這裡就涉及到了作用域鏈的概念,每個函式都有一個作用域鏈,這個作用域連結串列示的是這個函式能夠訪問哪些變數。一個函式初始化的時候,它的作用域鏈會被建立,並被掛載到global物件的[[Scope]]屬性上,這個屬性是一個內部屬性,我們訪問不了。作用域鏈其實是一個陣列,這個陣列從前往後依次表示的是這個函式能夠訪問的環境或者說外層函式。拿result[i]舉例,它初始化的時候,作用域鏈中有兩個物件,一個是createFunction的變數物件,一個是全域性變數物件。createFunction的變數物件是所有的可在createFunction內可以訪問的變數,包括函式引數、內部變數、內部函式。全域性變數物件就不用說了,所有函式的作用域鏈頂端都是全域性變數物件。當函式執行的時候,該函式內部可訪問的變數會變成一個變數物件,也被叫做活動物件,被加入導該函式的作用域鏈的最前端。函式執行解析變數時,會沿著作用域鏈依次尋找,直到找到位置。有一點需要注意,作用域鏈中的變數物件是一個引用,並沒有把變數物件複製再單獨儲存一份。

上面我們作用域鏈的概念和函式尋找變數的機制說明白了,下面我們來看看resultFun[6]在定義和執行時都發生了什麼。resultFun[6]初始化的時候,它的作用域鏈被建立,此時作用域鏈中包含兩個變數物件,一個是createFunction的變數物件,一個是全域性變數物件。createFunction的變數物件中有resulti兩個變數。一般當一個函式執行完之後,它的變數物件都會被銷燬,記憶體被回收。但是當createFunction執行完之後,result這個函式陣列被賦值給全域性的resultFun變數了,這10個函式還存在,而且這10個函式的作用域鏈中都有對createFunction的變數物件的引用,所以createFunction的變數物件不會被銷燬。createFunction執行完之後,它的變數物件中的變數i等於10。也就是說resultFun這10個函式作用域鏈中引用的變數物件中的i等於10。所以無論呼叫哪個函式,結果都是10。

我們解釋完為什麼了,面試官可能會問你,怎麼改一下讓它達到我們的預期效果。答案如下:

function createFunction() {
    var result = new Array();

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

    return result;
}
var resultFun = createFunction();
複製程式碼

好,你再解釋一下為什麼這樣就會達到預期效果(內心os:面試官好煩人)。我們上面的思路再看一下這個新函式,這個函式不同的地方在於給result賦值的函式,由一個匿名的函式立即執行表示式返回。我們可以發現最後result的值,其實是立即執行函式表示式的閉包。我們有10個立即執行函式表示式,就是10個對應的變數物件,result這10個函式的作用域鏈分別引用了這10個變數物件。我們知道,函式傳參是按值傳遞,所以num和i沒有關聯關係。這10個變數物件中num的值分別是從1到10。所以resultFun[6]()返回的是6。

從以上分析可以看出,閉包會使外層函式的變數物件不銷燬,佔用記憶體空間,所以要注意不要濫用閉包。

應用

私有變數

js的物件沒有私有屬性這個概念,所有的屬性都是公共的。我們可以利用閉包來達到私有變數的封裝。原理其實很簡單,我們在外部函式中定義一個變數,然後在函式內部定義函式,也就是閉包,閉包可以訪問該變數,然後返回該函式。那麼我們就只能通過這個閉包函式去訪問這個變數,這個變數就是私有變數。這個函式就被成為特權函式。

一般有兩種方式定義私有變數,一種是建構函式,另一種是立即執行函式表示式。

建構函式的私有變數

function Person(name){
    this.getName = function(){
        return name;
    };
    this.setName = function (value) {
        name = value;
    };
}
var person = new Person(“Nicholas”);
alert(person.getName()); //”Nicholas”
person.setName(“Greg”);
alert(person.getName()); //”Greg”
複製程式碼

函式引數name就是私有變數。每個例項上都有getName和setName,不共用。每個例項都對應不同的變數物件。這裡需要說明一下的是,函式每執行一次都會生成一個變數物件,叫做活動物件。對於內部有閉包的函式,每執行一次,生成一個變數物件,且該變數物件不銷燬,也就是說每執行一次,記憶體中就多一個變數物件。這種方式的確點就是例項的方法不共用,造成記憶體浪費。

靜態私有變數

我們可以使用立即函式表示式來實現:

(function(){
    var name = "";
    Person = function(value){
        name = value;
    };
    Person.prototype.getName = function(){
        return name;
    };
    Person.prototype.setName = function (value){
        name = value;
    };
})();
var person1 = new Person(“Nicholas”);
alert(person1.getName()); //”Nicholas”
person1.setName(“Greg”);
alert(person1.getName()); //”Greg”
var person2 = new Person(“Michael”);
alert(person1.getName()); //”Michael”
alert(person2.getName()); //”Michael”
複製程式碼

我們可以看到現在每個例項的方法和私有變數都是共有的了。使用建構函式私有變數還是靜態私有變數或者混合使用看你自己的需求。

模組模式(單例模式)

在只需要一個例項的情況下,我們可以這樣:

var application = function(){
    //private variables and functions
    var components = new Array();
    //initialization
    components.push(new BaseComponent());
    //public interface
    return {
        getComponentCount : function(){
            return components.length;
        },
        registerComponent : function(component){
            if (typeof component == “object”){
                components.push(component);
            }
        }
    };
}();
複製程式碼

相關文章