JavaScript 匿名函數

匿名函數就是沒有名字的函數,有時候也稱爲拉姆達(lambda)函數。匿名函數是一種強大的使人難以置信的工具,其用途很是之多,來看看下面這個典型的函數聲明:前端

function functionName(arg0, arg1, arg2) {
    //函數體
}

 既能夠像上面這樣聲明函數,也能夠像下面這樣以函數表達式的形式定義函數:web

var functionName = function(arg0, arg1, arg2) {
    //函數體
};

 雖然這兩個例子在邏輯上等價,但它們之間仍是存在一些區別。固然,函數聲明與函數表達式之間的主要區別,就是前者會在代碼執行之前被加載到做用域中,然後者則是在代碼執行到哪一行的時候纔會有定義。另外一個重要的區別是函數聲明會給函數指定一個名字,而函數表達式則是建立一個匿名函數,而後將這個匿名函數賦給一個變量。換句話說,上面第二個例子建立了帶有三個參數的匿名函數,而後把這個匿名函數賦給了變量functionName,可是,並無給匿名函數指定名字。數組

像下面這樣寫一個匿名函數也是能夠的:安全

function(arg0, arg1, arg2) {
    //函數體
}

 這些代碼徹底有效,但問題是,誰也不可能調用這個函數,由於沒有指向這個函數的指針。不過,在將函數做爲參數傳入另外一個函數,或者從一個函數中返回另外一個函數時,一般都要使用以這種形式來定義匿名函數。下面是曾經使用過的一個createComparisonFunction()函數的例子:閉包

function createComparisonFunction(propertyName) {
    return function(object1, object2) {
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];

        if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else {
            return 0;
        }
    };
}

 createComparisonFunction()就返回了一個匿名函數,返回的函數可能會被賦值給一個變量,或者以其餘方式被調用,不過,在createComparisonFunction()函數內部,它是匿名的,在把函數當成值的狀況下,均可以使用匿名函數。不過,這並非匿名函數的惟一用途。app


 

1 遞歸函數

遞歸函數是在一個函數經過名字調用自身的狀況下構成的,以下所示:工具

function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * factorial(num - 1);
    }
}

這是一個經典的遞歸階乘函數,雖然這個函數表面看來沒什麼問題,但下面的代碼卻可能致使它出錯:this

var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //VM848:5 Uncaught TypeError: factorial is not a function(…)

以上代碼先把factorial()函數保存在變量anotherFactorial中,而後將factorial變量設置爲null,結果指向原始函數的引用只剩下一個。但在接下來調用anotherFactorial()時,因爲必須執行factorial(),而factorial已經再也不是函數,因此就會致使錯誤。在這種狀況下,使用arguments.callee可解決這個問題:spa

function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);     }
}

var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //24

 加黃色背景的代碼顯示,經過使用arguments.callee代替函數名,能夠確保不管怎樣調用函數都不會出現問題。所以,在編寫遞歸函數時,使用arguments.callee總比使用函數名更保險。


 

2 閉包

有很多開發人員老是搞不清匿名函數和閉包這兩個概念,所以常常混用。閉包是指有權訪問另外一個函數做用域中的變量的函數建立閉包的常見方式,就是在一個函數內部建立另外一個函數,仍之前面的createComparisonFunction()函數爲例,注意加黃色背景的代碼:

function createComparisonFunction(propertyName) {
    return function(object1, object2) {
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];

        if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else {
            return 0;
        }
    };
}

 在這個例子中,突出的那兩行代碼是內部函數(一個匿名函數)中的代碼,這兩行代碼訪問了外部函數中的變量propertyName,即便這個內部函數被返回了,並且是在其餘地方被調用了,但它仍然能夠訪問變量propertyName。之因此還可以訪問這個變量,是由於內部函數的做用域鏈中包含createComparisonFunction()的做用域。要完全搞清楚其中的細節,必須從理解函數第一次被調用的時候都會發生什麼入手。

有關如何建立做用域鏈以及做用域鏈有什麼做用的細節,對完全理解閉包相當重要。當某個函數第一次被調用時,會建立一個執行環境(execution context)及相應的做用域鏈,並把做用域鏈賦值給一個特殊的內部屬性(即[[Scope]])。而後,使用this.arguments和其餘命名參數的值來初始化函數的活動對象(activation object)。但在做用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,...直至做爲做用域鏈終點的全局執行環境。

在函數執行過程當中,爲讀取和寫入變量的值,就須要在做用域中查找變量,來看下面的例子:

function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value2 > value1) {
        return 1;
    } else {
        return 0;
    }
}

var result = compare(5, 10); //-1

 以上代碼先定義了compare()函數,而後又在全局做用域中調用了它,當第一次調用compare()時,會建立一個包含this、arguments、value1和value2的活動對象。全局執行環境的變量對象(this、compare和result)在compare()執行環境的做用域鏈中則處於第二位。下圖展現了包含上述關係的compare()函數執行時的做用域鏈:

後臺的每一個執行環境都有一個表示變量的對象——變量對象。全局環境的變量對象始終存在,而像compare()函數這樣的局部環境的變量對象,則只在函數執行的過程當中存在。在建立compare()函數時,會建立一個預先包含全局變量對象的做用域鏈,這個做用域鏈被保存在內部的[[Scope]](scope chain)屬性中。當調用compare()函數時,會爲函數建立一個執行環境,而後經過複製函數的[[Scope]](scope chain)屬性中的對象構建起執行環境的做用域鏈。此後,又有一個活動對象(在此做爲變量對象使用)被建立並被推入執行環境做用域鏈的前端。對於這個例子中的compare()函數的執行環境而言,其做用域鏈中包含兩個對象,本地活動對象和全局活動對象。顯然,做用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。

不管何時在函數中訪問一個變量時,就會從做用域鏈中搜索具備相應名字的變量,通常來講,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域(全局執行環境的變量對象),可是閉包的狀況又有所不一樣。

但另外一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的做用域鏈中。所以,在createComparisonFunction()函數內部定義的匿名函數的做用域鏈中,實際上將會包含外部函數createComparisonFunction()的活動對象。下圖展現了當下列代碼執行時,包含函數與內部匿名函數的做用域鏈:

var compare = createComparisonFunction('name');
var result = compare({ name: 'Nicholas' }, { name: 'Greg' });

在匿名函數從createComparisonFunction()中被返回後,它的做用域鏈被初始化爲包含createComparisonFunction()函數的活動對象和全局變量對象。這樣,匿名函數就能夠訪問在createComparisonFunction()中定義的全部變量。更爲重要的是,createComparisonFunction()函數在執行完畢後,其活動對象也不會被銷燬,由於匿名函數的做用域鏈仍然在引用這個活動對象。換句話說,當createComparisonFunction()函數返回後,其執行環境的做用域鏈會被銷燬,但它的活動對象仍然會留在內存中,直到匿名函數被銷燬後,createComparisonFunction()的活動對象纔會被銷燬。例如:

//建立函數
var compareNames = createComparisonFunction('name');

//調用函數
var result = compareNames({ name: 'Nicholas' }, { name: 'Greg' });

//刪除對匿名函數的引用(以便釋放內存)
compareNames = null;

首先建立的比較函數被保存在變量compareNames中,而經過compareNames設置爲等於null解除該函數的引用,就等於通知垃圾回收例程將其清除。隨着匿名函數的做用域鏈被銷燬,其餘做用域(除了全局做用域)也均可以安全地銷燬了。上圖展現了調用compareNames()的過程當中產生的做用域鏈之間的關係。(因爲閉包會攜帶包含它的函數的做用域,所以會比其它函數佔用更多的內存。過分使用閉包可能會致使內存佔用過多,咱們建議讀者只在絕對必要時再考慮使用閉包。)

2.1 閉包與變量

做用域鏈的這種配置機制引出了一個值得注意的反作用,即閉包只能取得包含函數中任何變量的最後一個值。別忘了閉包所保存的是整個變量對象,而不是某個特殊的變量。下面這個例子可用清晰地說明這個問題:

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

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

    }

    return result;
}

var funcs = createFunctions();

//每一個函數都輸出10
for (var i = 0; i < funcs.length; i++) {
    document.write(funcs[i]() + '<br />');
}

 這個函數會返回一個函數數組,表面上看,彷佛每一個函數都應該返回本身的索引值,即位置0的函數返回0,位置1的函數返回1,以此類推。但實際上,每一個函數都返回10。由於每一個函數的做用域鏈中都保存着createFunctions()函數的活動對象。因此它們引用的都是同一個變量i當createFunctions()函數返回後,變量i的值是10,此時每一個函數都引用着保存變量i的同一個變量對象,因此在每一個函數內部i的值都是10,可是,咱們能夠經過建立另外一個匿名函數讓閉包的行爲符合預期,以下所示:

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

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

    return result;
}

var funcs = createFunctions();

//分別輸出0,1,2,3...
for (var i = 0; i < funcs.length; i++) { document.write(funcs[i]() + '<br />') }

 在重寫了前面的createFunctions()函數後,每一個函數就會返回各自不一樣的索引值了。在這個版本中,咱們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將當即執行該匿名函數的結果賦給數組。這裏的匿名函數有一個參數num,也就是最終的函數要返回的值。在調用每一個匿名函數時,咱們傳入了變量i。因爲函數參數是按值傳遞的,因此就會將變量i的當前值複製給參數num,而在這個匿名函數內部,又建立並返回了一個訪問num的閉包。這樣一來,result數組中的每一個函數都有本身num變量的一個副本。所以就能夠返回各自不一樣的數值了。

2.2關於this對象

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

var name = 'The Window';

var object = {
    name: 'My Object',

    getNameFunc: function() {
        return function() {
            return this.name;
        };
    }
};

alert(object.getNameFunc()()); //"The Window"

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

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

var name = 'The Window';

var object = {
    name: 'My Object',

    getNameFunc: function() {
        var that = this;
        return function() {
            return that.name;
        };
    }
};

alert(object.getNameFunc()()); //"My Object"

 代碼中突出的行展現了這個例子與前一個例子之間的不一樣之處,在定義匿名函數以前,咱們把this對象賦值給了一個名叫that的變量。而在定義了閉包以後,閉包也能夠訪問這個變量,由於它是咱們在包含函數中特地聲明的一個變量。即便在函數返回以後,that也仍然引用着object,因此調用object.getNameFunc()就返回了"My Object"。(this和arguments也存在一樣的問題,若是想訪問做用域中的arguments對象,必須將對該對象的引用保存到另外一個閉包可以訪問的變量中。)

2.3 內存泄漏

因爲IE對JScript對象和COM對象使用不一樣的垃圾收集例程,所以閉包在IE中會致使一些特殊的問題。具體來講,若是閉包的做用域鏈中保存着一個HTML元素,那麼就意味着該元素將沒法被銷燬。來看下面的例子:

function assignHandler() {
    var element = document.getElementById('someElement');
    element.onclick = function() {
        alert(element.id);
    };
}

 以上代碼建立了一個做爲element元素事件處理程序的閉包,而這個閉包則又建立了一個循環引用(事件將在後面篇章中討論)。因爲匿名函數保存了一個對assignHandler()的活動對象的引用,所以就會致使沒法減小element的引用數。只要匿名函數存在,element的引用數至少是1,所以它佔用的內存就永遠不會被回收。不過,這個問題能夠經過稍微改寫一下代碼來解決。以下所示:

function assignHandler() {
    var element = document.getElementById('someElement');
    var id = element.id;

    element.onclick = function() {
        alert(id);
    };

    element = null;
}

 在上面的代碼中,經過把element.id的一個副本保存在一個變量中,而且在閉包中引用該變量消除了循環引用。但僅僅作到這一步,仍是不能解決內存泄漏的問題。必需要記住,閉包會引用包含函數的整個活動對象,而其中包含着element。即便閉包不直接引用element,包含函數的活動對象中也仍然會保存一個引用,所以,有必要把element變量設置爲null,這樣就能解除對DOM對象的引用,順利地減小其引用數,確保正常回收其佔用的內存。


 

3. 模仿塊級做用域

如前所述,JavaScript沒有塊級做用域的概念,這意味着在塊語句中定義的變量,其實是在包含函數中而非語句中建立的,來看下面的例子:

function outputNumbers(count) {
    for (var i = 0; i < count; i++) {
        alert(i);
    }
    alert(i); //count
}

 這個函數中定義了一個for循環,而變量i的初始值被設置爲0。在Java、C++等語言中,變量i只會在for循環的語句塊中有定義,循環一旦結束,變量i就會被銷燬。但是在JavaScript中,變量i是定義在outputNumbers()的活動對象中的,所以從它有定義開始,就能夠在函數內部隨處訪問它。即便像下面這樣錯誤地從新聲明一個變量,也不會改變它的值

function outputNumbers(count) {
    for (var i = 0; i < count; i++) {
        alert(i);
    }
    var i; //從新聲明變量
    alert(i); //count
}

 JavaScript歷來不會告訴你是否屢次聲明瞭同一個變量,遇到這種狀況,它只會對後續的聲明視而不見(不過,它會執行後續聲明中的變量初始化)。匿名函數能夠用來模仿塊級做用域並避免這個問題

用做塊級做用域(一般稱爲私有做用域)的匿名函數的語法以下所示:

(function() {
    //這裏是塊級做用域
})();

 以上代碼定義並當即調用了一個匿名函數,將函數聲明包含在一對圓括號中,表示它其實是一個函數表達式。而緊隨其後的另外一對圓括號會當即調用這個函數。若是有讀者感受這種語法不太好理解,能夠再看看下面的例子:

var count = 5;
outputNumbers(count);

 這裏初始化了變量count,將其值設置爲5,固然,這裏的變量是沒有必要的,由於能夠把值直接傳給函數,爲了讓代碼更簡潔,咱們在調用函數時用5來調用變量count,以下所示:

outputNumbers(5);

 這樣作之因此可行,是由於變量只不過是值的另外一種表現形式,所以用實際的值替換變量沒有問題,再看下面的例子:

var someFunction = function() {
    //這裏是塊級做用域
};
someFunction();

 這個例子先定義了一個函數,而後當即調用了它。定義函數的方式是建立一個匿名函數,並把匿名函數賦值給變量someFunction。而調用函數的方式是在函數名稱後面添加一對圓括號,即someFunction()。經過前面的例子咱們知道,可用使用實際的值來取代變量count,那在這裏是否是也能夠用函數的值直接取代函數名呢?然而,下面的代碼卻會致使錯誤:

function() {
    //這裏是塊級做用域
}(); //出錯

 

這段代碼會致使語法錯誤,是由於JavaScript將function關鍵字看成一個函數聲明的開始,而函數聲明後面不能跟圓括號,然而,函數表達式的後面能夠跟圓括號。要將函數聲明轉換成函數表達式,只要像下面這樣給它加上一對圓括號便可:

(function() {
    //這裏是塊級做用域
})();

 不管在什麼地方,只要臨時須要一些變量,就可使用私有做用域,例如:

function outputNumbers(count) {
    (function() {
        for (var i = 0; i < count; i++) {
            alert(i);
        }
    })();
    alert(i); //致使一個錯誤
}

 在這個重寫後的outputNumbers()函數中,咱們在for循環外部插入了一個私有做用域。在匿名函數中定義的任何變量,都會在執行結束時被銷燬。所以,變量i只能在循環中使用,使用後即被銷燬。而在私有做用域中可以訪問變量count,是由於這個匿名函數是一個閉包,它可以訪問包含做用域中的全部變量。

這種技術常常在全局做用域中被用在函數外部,從而限制向全局做用域中添加過多的變量和函數。通常來講,咱們都應該儘可能少向全局做用域中添加變量和函數。在一個由不少開發人員共同參與的大型應用程序中,過多的全局變量和函數很容易致使命名衝突,而經過建立私有做用域,每一個開發人員既可使用本身的變量,又不比擔憂搞亂全局做用域。例如:

(function() {
    var now = new Date();
    if (now.getMonth() == 0 && now.getDate() == 1) {
        alert('Happy new year!');
    }
})();

 把上面這段代碼放在全局做用域中,可用用來肯定哪一天是1月1日,若是到了這一天,就會向用戶顯示一條祝賀新年的消息。其中的變量now如今是匿名函數中的局部變量,而咱們沒必要在全局做用域中建立它。(這種作法能夠減小閉包占用的內存問題,由於沒有指向匿名函數的引用,只要函數執行完畢,就能夠當即銷燬其做用域鏈了。


 

4 私有變量

嚴格來說,JavaScript中沒有私有成員的概念,全部對象屬性都是公有的,不過,卻是有一個私有變量的概念。任何在函數中定義的變量,均可以認爲是私有變量,由於不能在函數的外部訪問這些變量,私有變量包括函數的參數、局部變量和在函數內部定義的其餘函數。來看下面的例子:

function add(num1, num2) {
    var sum = num1 + num2;
    return sum;
}

 在這個函數內部,有三個私有變量:num1,num2,sum.在函數內部能夠訪問這幾個變量,但在函數外部則訪問不到它們。若是在這個函數內部建立一個閉包,那麼閉包經過本身的做用域鏈也能夠訪問這些變量。而利用這一點,就能夠建立用於訪問私有變量的共有方法。

咱們把有權訪問私有變量和私有函數的公有方法稱爲特權方法(privileged method)。有兩種在對象上建立特權方法的方式,第一種是在構造函數中定義特權方法,基本模式以下:

function MyObject() {

    //私有變量和私有函數
    var privateVariable = 10;

    function privateFunction() {
        return false;
    }

    //特權方法
    this.publicMethod = function() {
        privateVariable++;
        return privateFunction();
    };
}

 這個模式在構造函數內部定義了全部私有變量和函數。而後,又繼續建立了可以訪問這些私有成員的特權方法。可以在構造函數中定義特權方法,是由於特權方法做爲閉包有權訪問在構造函數中定義的全部變量和函數。對這個例子而言,變量privateVariable和函數privateFunction()只能經過特權方法publicMethod()來訪問。在建立MyObject的實例後,除了使用publicMethod()這一個途徑外,沒有任何方法能夠直接訪問privateVariable和privateFunction()。

利用私有和特權成員,可用隱藏那些不該該被直接修改的數據,例如:

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"

以上代碼的構造函數中定義了兩個特權方法:getName()和setName()。這兩個方法均可以在構造函數外部使用,並且都有權訪問私有變量name,但在Person構造函數外部,沒有任何辦法訪問name。因爲這兩個方法是在構造函數內部定義的,它們做爲閉包可以經過做用域鏈訪問name。私有變量name在Person的每個實例中都不相同,由於每次調用構造函數都會從新建立這兩個方法。不過,在構造函數中定義特權方法也有一個缺點,那就是你必須使用構造函數模式來達到這個目的,構造函數模式的缺點是針對每一個實例都會建立一組新方法,而使用靜態私有變量來實現特權方法就能夠避免這個問題。

4.1 靜態私有變量

經過在私有做用域中定義私有變量或函數,一樣也能夠建立特權方法,其基本模式以下:

(function() {

    //私有變量和私有函數
    var privateVariable = 10;

    function privateFunction() {
        return false;
    }

    //構造函數
    MyObject = function() {
    };

    //公有/特權方法
    MyObject.prototype.publicMethod = function() {
        privateVariable++;
        return privateFunction;
    };
})();

這個模式建立了一個私有做用域,並在其中封裝了一個構造函數及相應的方法。在私有做用域中,首先定義了私有變量和私有函數,而後又定義了構造函數及其公有方法。公有方法是在原型上定義的,這一點體現了典型的原型模式。須要注意的是,這個模式在定義構造函數模式時並無使用函數聲明,而是使用了函數表達式。函數聲明只能建立局部函數,但那並非咱們想要的。出於一樣的緣由,咱們也沒有在聲明MyObject時使用var關鍵字。記住,初始化未經聲明的變量,老是會建立一個全局變量。所以,MyObject就成了一個全局變量,可以在私有做用域以外被訪問到。

這個模式與在構造函數中定義的特權方法的主要區別,就在於私有變量和函數是由實例共享的。因爲特權方法是在原型上定義的,所以全部實例都使用同一個函數。而這個特權方法,做爲一個閉包,老是保存着對包含做用域的引用。來看一看下面的代碼:

(function() {

    var name = '';

    Person = function(value) {
        name = value;
    };

    Person.prototype.getName = function() {
        return name;
    };

    Person.prototype.setName = function(value) {
        name = value;
    };
})();

var person = new Person('Nicholas');
alert(person.getName()); //"Nicholas"
person.setName('Greg');
alert(person.getName()); //"Greg"

var person2 = new Person('Michael');
alert(person.getName()); //"Michael"
alert(person2.getName()); //"Michael"

 

這個例子中的Person構造函數與getName()和setName()方法同樣,都有權訪問私有變量name,在這種模式下,變量name就變成了一個靜態的、由全部實例共享的屬性,也就是說,在一個實例上調用setName()會影響全部實例。而調用setName()或新建一個Person實例都會賦予name屬性一個新值。結果就是全部實例都會返回相同的值。

以這種方式建立靜態私有變量會由於使用原型而增進代碼複用,但每一個實例都沒有本身的私有變量,究竟是使用實例變量、仍是靜態私有變量,最終仍是要視你的具體需求而定。(多查找做用域鏈中的一個層次,就會在必定程度上影響查找速度。而這正是使用閉包和私有變量的一個顯明的不足之處。)

4.2 模塊模式

前面的模式是用於爲自定義類型建立私有變量和特權方法的。而道格拉斯所說的模塊模式(module pattern)則是爲單例建立私有變量和特權方法。所謂單例(singleton),指的就是隻有一個實例的對象。按照慣例,JavaScript是以對象字面量的方式來建立單例對象的:

var singleton = {
    name: value,
    method: function() {
        //這裏是方法的代碼
    }
};

 模塊模式經過爲單例添加私有變量和特權方法可以使其獲得加強,其語法形式以下:

var privateVariable = function() {

    //私有變量和私有函數
    var privateVariable = 10;

    function privateFunction() {
        return false;
    }

    //特權/共有方法和屬性
    return {
        publicProperty: true;
        publicMethod: function() {
            privateVariable++;
            return privateFunction();
        }
    };
}();

 這個模塊模式使用了一個返回對象的匿名函數。在這個匿名函數內部,首先定義了私有變量和函數,而後,將一個對象字面量做爲函數的值返回。返回的對象字面量中只包含能夠公開的屬性和方法。因爲這個對象是在匿名函數內部定義的,所以它的公有方法有權訪問私有變量和函數。從本質來說,這個對象字面量定義的是單例的公共接口。這種模式在須要對單例進行某些初始化,同時又須要維護其私有變量時是很是有用的。 例如:

function BaseComponent() {}

function OtherComponent() {}

var application = function() {

    //私有變量和函數
    var components = new Array();

    //初始化
    components.push(new BaseComponent());

    //公共
    return {
        getComponentCount: function() {
            return components.length;
        },
        registerComponent: function(component) {
            if (typeof component == 'object') {
                components.push(component);
            }
        }
    };
}();

application.registerComponent(new OtherComponent());
alert(application.getComponentCount()); //2

 在web應用程序中,常常須要使用一個單例來管理應用程序級的信息,這個簡單的例子建立了一個用於管理組件的application對象,在建立這個對象的歷程中,首先聲明瞭一個私有的components數組,並向數組中添加了一個BaseComponent的新實例(在這裏不須要關心BaseComponent的代碼,咱們只是用它來展現初始化操做)。而返回對象的getComponentCount()和registerComponent()方法,都是有權訪問components的特權方法。前者只是返回已註冊的組件數目,後者用於註冊新組件。

簡言之,若是必須建立一個對象並以某些數據對其進行初始化,同時還要公開一些可以訪問這些私有數據的方法,那麼就可使用模塊模式,以這種模式建立的每一個單例都是Object的實例。由於最終要經過一個對象字面量來表示它。事實上,這也沒有什麼,畢竟,單例一般都是做爲全局對象存在的。咱們沒必要將它傳遞給一個函數,所以,也就沒有必要使用instanceof操做符來檢查其對象類型了。

4.3 加強的模塊模式

有人進一步改進了模塊模式,即在返回對象以前加入對其加強的代碼。這種加強的模塊模式適合那些單例必須是某種類型的實例,同時還必須添加某些屬性和(或)方法對其加以加強的狀況。來看下面的例子:

var singleton = function() {

    //私有變量和私有函數
    var privateVariable = 10;

    function privateFunction() {
        return false;
    }

    //建立對象
    var object = new CustomType();

    //添加特權/公有屬性和方法
    object.publicProperty = true;

    object.publicMethod = function() {
        privateVariable++;
        return privateFunction();
    };
    //返回這個對象
    return object;
}();

若是前面演示模塊模式的例子中的application對象必須是BaseComponent的實例,那麼就可使用如下代碼:

var application = function() {

    //私有變量和函數
    var components = new Array();

    //初始化
    components.push(new BaseComponent());

    //建立application的一個局部副本
    var app = new BaseComponent();

    //公共接口
    app.getComponentCount = function() {
        return components.length;
    }

    app.registerComponent = function(component) {
        if (typeof component == 'object') {
            components.push(component);
        }
    };

    //返回這個副本
    return app;
}();

在這個重寫後的應用程序(applicaiton)單例中,首先也是像前面例子中同樣定義了私有變量,主要的不一樣之處在於命名變量app的建立過程。由於它必須是BaseComponent的實例,這個實例其實是 applicaiton對象的局部變量版。此後,咱們又爲app對象添加了可以訪問私有變量的公有方法。最後一步是返回app對象,結果仍然是將它賦值給全局變量applicaiton.

5 小結

匿名函數,也稱爲拉姆達函數,是一種使用JavaScript函數的強大方式,如下總結了匿名函數的特色:

a,任何函數表達式從技術上說都是匿名函數,由於沒有引用它們的肯定的方式;

b,在沒法肯定如何引用函數的狀況下,遞歸函數就會變得比較複雜;

c,遞歸函數應該始終使用arguments.callee來遞歸地調用自身,不要使用函數名——函數名可嫩會發生變化;

當在函數內部定義了其餘函數時,就建立了閉包。閉包有權訪問包含函數內部的全部變量,原理以下:

a,在後臺執行環境中,閉包的做用域鏈包含着它本身的做用域、包含函數的做用域和全局做用域;

b,一般,函數的做用域及其全部變量都會在函數執行結束後被銷燬;

e,但時,當函數返回了一個閉包時,這個函數的做用域將會一直在內存中保存到閉包不存在爲止,使用閉包能夠在JavaScript中模仿塊級做用域(JavaScript自己沒有塊級做用域的概念),要點以下:

a,建立並當即調用一個函數,這樣既能夠執行其中的代碼,又不會在內存中留下對該函數的引用;

b,結果就是函數內部的全部變量都會被當即銷燬——除非將這些變量賦值給了包含做用域(即外部做用域)中的變量。

閉包還能夠用於在對象中建立私有變量,相關概念和要點以下:

a,即便JavaScript中沒有正式的私有對象屬性的概念,但可使用閉包來實現公有方法,而經過共有方法能夠訪問在包含做用域中定義的變量;

b,有權訪問私有變量的公有方法叫作特權方法;

c,可用使用構造函數模式、原型模式來實現自定義類型的特權方法,也可使用模塊模式、加強的模塊模式來實現單例的特權方法;

JavaScript中的匿名函數和閉包都是很是有用的特性,利用它們能夠實現不少功能,不過,由於建立閉包必須維護額外的做用域,因此過分使用它們可能會佔用大量內存。

相關文章
相關標籤/搜索