javascript:物件導向的程式設計

南郭竽發表於2018-05-02

JavaScript讀書筆記

備註:為了防止標籤錯亂,現在規定,標題用 ## ,一級標題是### , 二級是#### , 三級是 #####

物件導向的程式設計

內容(3點):

函式表示式的特徵

使用函式實現遞迴

使用閉包定義私有變數

函式表示式是Javascript中的一個既強大又容易令人困惑的特性。前面說過,定義函式的方式有兩種:一種是函式宣告;另一種就是函式表示式。函式宣告的語法是這樣的。

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

首先是function關鍵字,然後是函式的名字,這就是指定函式名的方式。Firefox,Safari,ChromeOpera都給函式定義了一個非標準的name屬性,通過這個屬性可以訪問到給函式指定的名字。這個屬性的值永遠等於跟在function關鍵字後面的識別符號。

// 只在上述瀏覽器生效
console.log(functionName.name); // 'functionName'

關於函式宣告,它的一個重要特徵就是函式宣告提升(function declaration hoisting),意思是在執行程式碼之前會先讀取函式宣告。這久意味著可以把函式宣告放在呼叫它的語句後面。

sayHi();
function sayHi(){
    console.log('hi');
}

這個例子不會丟擲錯誤,因為在程式碼執行之前就會先讀取函式宣告。

第二種建立函式的方式是使用函式表示式。函式表示式有幾種不同的語法形式。下面是最常見的一種形式。

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

這種形式看起來好像是常規的變數賦值語句,即建立一個函式並將它賦值給變數functionName。這種情況下建立的函式叫做匿名函式(anonymous function),因為function關鍵字後面沒有識別符號。匿名函式的name屬性是空字串。

函式表示式與其他表示式一樣,在使用前必須先賦值。以下程式碼會導致錯誤。

sayHi(); // 錯誤:函式還不存在

var sayHi = function(){
    console.log('hi');
}

理解函式提升的關鍵,就是理解函式宣告與函式表示式之間的區別。例如,執行以下程式碼的結果可能會讓人意想不到。

// 不要這樣做

if (condition){
    funcion sayHi(){
        console.log('hi');
    }
}else {
    funcion sayHi(){
        console.log('Yo!');
    }
}

由於函式宣告提升,這個程式碼在不同的瀏覽器會出現不同的效果。

不過,如果使用函式表示式,就沒有問題。

var sayHi;
if (condition){
    sayHi = function(){
        console.log('hi');
    }
} else {
    sayHi = function() {
        console.log('Yo!');
    }
}

這個例子不會有什麼意外,不同的函式會根據condition被賦值給sayHi

能夠建立函式再賦值給變數,也能夠把函式作為其他函式的返回值。


function returnFunc(){
    return function(){
        console.log('被慾望玷汙的亞瑟');
    }
}

// 呼叫該函式

var innerF = returnFunc()
innerF(); // 呼叫內部函式~

1 遞迴

遞迴函式是在一個函式通過名字呼叫自身的情況下構成的。如下:

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

會出問題:如果函式被重新賦值

var af = factorial;
factorial = null;

console.log(af(5)); // 出錯

解決方案:內部使用arguments.callee.

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

因為 arguments,callee是一個指向正在執行的函式的指標。

不過嚴格模式下,不能通過指令碼訪問arguments.callee,訪問這個屬性會導致錯誤。

改進:

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

console.log(factorial(6));
var af = factorial;
console.log(af(7)); // ok

2 閉包

有不少開發人員總是搞不清匿名函式閉包這兩個概念,因此經常混用。閉包是指有權訪問另一個函式作用域中的變數的函式。建立閉包的常見方式,就是在一個函式內部建立另一個函式。如下:

function createComparisonFunction(prpertyName){
    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;
        }
    }
}

注意其中的

var value1 = obj1[propertyName];
var value2 = obj2[propertyName];

這兩行程式碼位於內部函式(一個匿名函式)中。這兩行程式碼訪問了外部函式中的變數propertyName。即使這個內部函式被返回了,而且是在其他地方被呼叫了,但它仍然可以訪問變數propertyName之所以還能夠訪問這個變數,是因為內部函式的作用域鏈中包含createComparisonFunction()的作用域。要徹底搞清楚其中的細節,必須從理解函式第一次被呼叫的時候都會發生什麼入手。

有關如何建立作用域鏈以及作用域鏈有什麼作用的細節,對徹底理解閉包至關重要。當某個函式第一次被呼叫時,會建立一個執行環境(execution context)及相應的作用域鏈,並把作用域鏈賦值給一個特殊的內部屬性(即[Scope])。然後,使用this,arguments和其他命名引數的值來初始化函式的活動物件(activation object)。但在作用域鏈中,外部函式的活動物件始終處於第二位,外部函式的外部函式的活動物件處於第三位,……直至作為作用域鏈終點的全域性執行環境。

在函式執行的過程中,為讀取和寫入變數的值,就需要在作用域鏈中查詢變數。如下:

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

var result = compare(5,10);

以上程式碼先定義了compare()函式,然後又再全域性作用域中呼叫了它。當第一次呼叫compare()時,會建立一個包含this,arguments,v1,v2的活動物件。全域性執行環境的變數物件(包含this,result,compare)在compare()執行環境的作用域鏈中則處於第二位。

後臺的每個執行環境都有一個表示變數的物件————變數物件。全域性環境的變數物件始終存在,而像compare()函式這樣的區域性環境的變數物件,則只在函式執行的過程中存在。在建立compare()函式時,會建立一個預先包含全域性變數物件的作用域鏈,這個作用域鏈被儲存到內部的[[Scope]]屬性中。當呼叫compare()函式時,會為函式建立一個執行環境,通過複製函式的[[Scope]]屬性中物件構建起執行環境的作用域鏈。此後,又一個活動物件(在此作為變數物件使用)被建立並推入執行環境作用域鏈的前端。對於這個例子中的compare()函式的執行環境而言,其作用域鏈中包含兩個變數物件:本地活動物件和全域性變數物件。顯然,作用域鏈本質上是一個指向變數物件的指標列表,它只引用但不實際包含變數物件。

無論什麼時候在函式中訪問一個變數時,就會從作用域鏈中搜尋具有相應名字的變數。一般來講,當函式執行完畢後,區域性活動物件就會被銷燬,記憶體中僅僅儲存全域性作用域(全域性執行環境的變數物件)。但是,閉包的情況又有所不同

在另一個函式內部定義的函式會講包含函式(即外部函式)的活動物件新增到它的作用域中。因此在createComparisonFunction()函式內部定義的匿名函式的作用域鏈中,實際將包含外部函式createComparisonFunction()的活動物件。

var compare = createComparisonFunction('name');
var result = compare({name:'Ann'} , {name:'Rose'});

在匿名函式從 createComparisonFunction()中被返回後,它的作用域鏈被初始化為包含createComparisonFunction()函式的活動物件和全域性變數物件。這樣,匿名函式就可以訪問createComparisonFunction()中定義的所有變數。更為重要的是,createComparisonFunction()函式執行完畢後,其活動物件也不會銷燬,因為匿名函式的作用域鏈仍然在引用這個活動物件。換句話說,當createComparisonFunction()函式返回後,其執行環境的作用域鏈會被銷燬,但它的活動物件仍然會留在記憶體中;直到匿名函式被銷燬後,createComparisonFunction()的活動物件才會被銷燬。如下:

// 建立函式
var compare = createComparisonFunction('name');
// 呼叫函式
var result = compare({name:'Ann'} , {name:'Rose'});
// 解除對匿名函式的引用(以便釋放記憶體)
compare = null;

首先,建立的比較函式被儲存在變數compare中。而通過將compare設定為null解除該函式的引用,就等於通知垃圾回收例程將其清除。隨著匿名函式的作用域鏈被銷燬,其他作用域(除了全域性作用域)也可以安全地銷燬了。

由於閉包會攜帶包含它的函式的作用域,因此會比其他函式佔用更多的記憶體。過度使用閉包可能會導致記憶體佔用過多,建議只在絕對必要時才考慮使用閉包。雖然像V8等優化後的Javascript引擎會嘗試回收被閉包占用的記憶體,但是還是要慎重使用閉包。

2.1 閉包與變數

作用域鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能取得包含函式中的任何變數的最後一個值。別忘了閉包所儲存的是整個變數物件,而不是某個特殊的變數。下面的程式碼可以清晰地說明這個問題。

// 問題閉包
function createFuncArr() {
    var result = [];

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

    return result;
}

var funcArr = createFuncArr();

for (var index = 0; index < funcArr.length; index++) {
    console.log(funcArr[index]());
}

輸出10次 110。

ps:為什麼要使用索引遍歷陣列:javascript 四種陣列遍歷方法

這個函式會返回一個陣列。表面上看,似乎每個函式都應該返回自己的 索引值+100,即位置0的函式返回0+100,位置1的函式返回1+100,以此類推。但實際上,每個函式都返回10+100。因為每個函式的作用域中都儲存著createFuncArr()函式的活動物件,所有它們引用的都是同一個變數i。當createFuncArr()函式返回後,變數i的值是10,此時每個函式都引用著儲存變數i的同一個變數物件,所以在每個函式的內部i的值都是10。一下是一些解決方案:

// 非閉包的解決方案
function createFuncArr() {
    var result = [];

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

    return result;
}

var funcArr = createFuncArr();

for (var index = 0; index < funcArr.length; index++) {
    console.log(funcArr[index](index));
}

這裡不在內部函式中去引用外部函式的變數,而是讓呼叫者去傳遞進來,這樣當然就避免了這個問題。但是,如果呼叫者不是使用索引的方式去遍歷陣列,這就比較麻煩了。

// 閉包的解決方式:
function createFuncArr() {
    var result = [];

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

    return result;
}

var funcArr = createFuncArr();

for (var index = 0; index < funcArr.length; index++) {
    console.log(funcArr[index]());
}

採用閉包的解決方式,避免了上述的問題,不需要呼叫者傳遞索引值進來了。但是要注意一下為什麼這種寫法可行,不會出現之前的問題。

因為第一個匿名函式是立即執行的,立即獲得i的值,在每次遍歷的時候。也就每次獲得了對應的索引值。然後裡面的閉包,會引用這個index,因為每個index是不同的(本質上,是每個最裡面的閉包的[Scope] chain 是不同的),所以結果也就每個都不同了

這裡沒有直接把閉包賦值給陣列,而是定義了一個匿名函式,並將立即執行該匿名函式的結果賦給陣列。這裡的匿名函式有一個引數index,也就是最終的函式需要的返回值的組成部分。在呼叫每個匿名函式時,我們傳入了變數i。由於函式引數是按值傳遞的,所以就會將變數i的當前值複製給引數num。而這個匿名函式內部,又建立並返回了一個訪問index的閉包。這樣一來,result陣列中的每個函式都有自己的index變數的一個副本,因此就可以返回各自不同的值了。

ps:這個問題,如果在python中,一個簡單的解決方式就是設定預設引數;在ECMAScript6中似乎也開始支援預設引數了

2.2 關於this物件

在閉包中使用this物件也可能會導致一些問題。我們知道,this物件是執行時基於函式的執行環境繫結的:在全域性函式中,this等於window,而當函式被作為某個物件的方法呼叫是,this等於那個物件。不過,匿名函式的執行環境具有全域性性,因此其this物件通常指向window。但是由於編寫閉包的方式不同,這一點可能不會那麼明顯。如下:

name = 'Ann';
var obj = {
    name: 'Rose',
    getName: function () {
        return function () {
            return this.name;
        };
    }
};

console.log(obj.getName()()); // Ann

無閉包的情況下:

name = 'Ann';
var obj = {
    name: 'Rose',
    getName: function () {
        return this.name;
    }
};

console.log(obj.getName()); // Rose

關於javascript閉包中的this物件 這篇文章很清楚地解釋了這個現象。

來看一下在閉包的情況下執行

console.log(obj.getName()()); // Ann

這一句程式碼其實相當於下面兩句:

var func = obj.getName();
console.log(func());

在第一句程式碼(var func = obj.getName();)中的thisobj,而第二句程式碼,沒有被任何物件呼叫,所以第二句程式碼中的this就是全域性的this,也就是window;

解決方案:

name = 'Ann';
var obj = {
    name: 'Rose',
    getName: function () {
        other = this; // todo
        return function () {
            return other.name; // todo
        };
    }
};
console.log(obj.getName()()); // Rose

修改如上,通過對obj.getName()()的拆分,可以明確看到第二個函式的執行環境是全域性執行環境。而obj.getName()的執行環境是區域性的,thisobj,所以,在這裡,先取得this的值(other=this;),然後就可以去使用這個this的值(return other.name;)了。

ps:這種方式並沒有改變閉包的this(也無法改變),而是提前取得了內部的this,然後在外部去使用。

argumentsthis一樣存在這個問題。如果想訪問作用域中的arguments物件,必須將對該物件的引用儲存到另一個閉包能夠訪問的變數中

2.3 記憶體洩露 (大霧)

由於IE9之前的版本對Javascript物件和COM物件使用不同的垃圾收集例程,因此閉包在IE的這些版本中會導致一些特殊的問題。具體來說,如果閉包的作用域鏈中儲存著一個HTML元素,那麼就意味著該元素無法被銷燬。如下:

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

以上程式碼建立了一個作為element元素事件處理程式的閉包,而這個閉包又建立了一個迴圈引用。由於匿名函式儲存了一個對assignHandler()的活動物件的引用,因此就會導致無法減少element的引用數。只要匿名函式存在,element的引用數至少是1,因此它所佔用的記憶體就永遠不會被回收。不過,這個問題可以通過下面的程式碼解決:

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

在上面的程式碼中,通過把element.id的一個副本儲存在一個變數中,並且在閉包中引用該變數消除了迴圈引用。但是僅僅做到這一步,還是不能解決記憶體洩露的問題。必須要記住:閉包會引用包含函式的整個活動物件,而其中包含著element。即使閉包不直接引用element,包含函式的活動物件也仍然會儲存一個引用。因此,有必要把element變數設定為null。這樣就能夠消除對DOM物件的引用,順利地減少其引用輸出,確保正常回收其佔用的記憶體。

3 模仿塊級作用域

如前所述,Javascript中沒有塊級作用域的概念。這意味著,在語句塊中定義的變數,實際上是在包含函式中而非語句中建立的。如下:

function outputNumbers(count){
    for (var i=0; i<count; i++){
        console.log(i);
    }

    console.log(i); // 依然生效
}

即使是這樣,也不會報錯:

function outputNumbers(count){
    for (var i=0; i<count; i++){
        console.log(i);
    }
    var i;
    console.log(i); // 依然生效 --> 依然是 5 
}
outputNumbers(5);

Javascript從來不會告訴你是否多次宣告瞭同一個變數;遇到這種情況,它只會對後續的宣告視而不見(不過,它會執行後續宣告中的變數初始化)。匿名函式可以用來模仿塊級作用域並避免這個問題。

用塊級作用域(通常稱為私有作用域)的匿名函式的語法如下所示:

(function(){

})();

ps: 這裡為什麼要給匿名函式外部加一個圓括號?

第一:不加的話,會出現語法錯誤。不加表示定義了一個匿名函式,而定義的函式後面加一個()並不是呼叫,而是語法錯誤。

第二:加了只會,將函式定義變成了一個匿名函式表示式,然後是呼叫函式表示式(),這沒有任何問題。

// 利用匿名函式模仿塊級作用域

function outputNumbers(count){

    (function () {
        for (var i=0; i<count; i++){
            console.log(i);
        }
    })();
    console.log(i); // 報錯:ReferenceError: i is not defined
}

outputNumbers(5);

ps: 為什麼此匿名函式可以訪問外部函式中的count?因為此匿名函式實際上是一個閉包。

這種技術經常在全域性作用域中被用在函式外部,從而限制向全域性作用域中新增過多的變數和函式。一般來說,我們都應該儘量少向全域性作用域中新增變數和函式。在一個由很多開發人員共同參與的大型應用程式中,過多的全域性變數和函式很容易導致命名衝突。而通過建立私有作用域,每個開發人員既可以使用自己的變數,又不必擔心搞亂全域性作用域。如下示例:

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

這種做法可以減少閉包占用的記憶體問題,因為沒有指向匿名函式的引用。只要函式指向完畢,就可以立即銷燬其作用域鏈了。

4 私有變數

嚴格來講,Javascript中沒有私有成員的概念;所有物件的屬性都是公有的。任何在函式中定義的變數,都可以認為是私有變數,因為不能在函式外部訪問這些變數。私有變數包括函式的引數,區域性變數和在函式內部定義的其他函式。如下:

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

在這個函式內部,有3個私有變數:num1,num2sum。在函式內部可以訪問這幾個變數,但是在函式外部不能訪問它們。如果在這個函式內部建立了一個閉包,那麼閉包通過自己的作用域鏈也可以訪問這些變數。而利用這一點,就可以建立用於訪問私有變數的公有方法。(怎麼理解?)

我們吧有權訪問私有變數和私有函式的公有方法稱為特權方法privileged method)。有兩種在物件上建立特權方法的方式。

  • 第一種是在建構函式中定義特權方法,基本模式如下:
function MyObject(){
    // 私有變數和函式
    var privateVar = 10;

    function privateFunc(){
        return privateVar%2===0;
    }

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

}

var obj = new MyObject();

console.log(obj.publicMethod());
console.log(obj.publicMethod());
console.log(obj.publicMethod());

這個模式在建構函式內部定義了所以私有變數和函式。然後,又繼續建立了能夠訪問這些私有成員的特權方法。能夠在建構函式中定義特權方法,是因為特權方法作為閉包有權訪問在建構函式中定義的所有變數和函式。[廢話,不是閉包,只要是內部函式不都可以訪問外部函式中的區域性變數嗎!]對這個例子而言,變數privateVar和函式privateFunc()只能通過publicMethod()來訪問。在建立MyObject例項之後,除了使用publicMethod()這一個途徑外,沒有任何方法可以直接訪問privateVarprivateFunc()

利用私有和特權成員,可以隱藏那些不應該被直接修改資料。比如:

function Person(name){
    this.getName = function(){
        return name;
    }
    this.setName = function(value){
        name = value;
    }
}

不過,在建構函式中定義特權方法也有缺點,就像之前說的,建構函式中定義的方法,不能被所有當前型別的例項共享,造成記憶體的浪費。

4.1 靜態私有變數

通過在私有作用域中定義私有變數或函式,通用可以建立特權方法,其基本模式如下:

(function(){
    // 私有變數和函式
    var privateVar = 10;

    function privateFunc(){
        return privateVar%2===0;
    }
    // 建構函式

    MyObject = function(){
    }
    // 特權方法
    MyObject.prototype.publicMethod = function(){
        privateVar++;
        return privateFunc();
    }

})();

這部分內容差點讓我堅持不住了,我被這奇怪的邏輯給搞得暈頭轉向的。

這個模式建立了一個私有作用域(外部的匿名函式),並在其中封裝了一個建構函式及相應的方法。在私有作用域中,首先定義了私有變數和私有函式,然後又定義了建構函式及其公有方法。公有方法是在原型上定義的,這一點體現了典型的原型模式。需要注意的是,這個模式在定義建構函式時並沒有使用函式宣告,而是使用了函式表示式。函式宣告只能建立區域性函式(只能在私有作用域中被訪問),但這不是我們想要的。出於同樣的原因,我們也沒有在宣告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 p1 = new Person('tom');
var p2 = new Person('ann');
console.log(p1.getName()+' ### '+p2.getName());
p1.setName('張飛');
console.log(p1.getName()+' ### '+p2.getName());

輸出:

ann ### ann
張飛 ### 張飛

這個例子中的Person建構函式與getName()setName()方法一樣,都有權訪問私有變數name在這種模式下,變數name變成了一個靜態的、由所有例項共享的屬性。(怎麼理解由所有例項共享,為什麼會被所有例項共享?)也就是說,在一個例項上呼叫setName()會影響所有例項。而呼叫setName()或新建一個Person例項都會賦予name屬性一個新值。結果就是所有的例項都會返回相同的值

上面已經說明了為什麼name會被所有例項共享了。原因就是:[由於特權方法是在原型上定義的,因此所有例項都使用同一個函式。而這個特權方法,作為一個閉包,總是儲存著對包含作用域(也就是外部函式的作用域)的引用]. ===>[name就是在包含作用域中定義的變數。]

這個私有變數被所有例項共享,是兩個原因組合在一起導致的。第一個是原型方法會被所以例項共享;第二個是閉包總是儲存這對包含作用域的引用。(而原型方法,也就是這裡的閉包。)

4.2 模組模式

前面的模式是用於為自定義型別建立私有變數和特權方法,而道格拉斯所說的模組模式(module pattern)則是為單例建立私有變數和特權方法。所有單例(singleton),指的是隻有一個例項的物件。按照慣例,Javascript是以物件字面量的方式來建立單例物件的。

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

模組模式通過為單例新增私有變數和特權方法能夠使其得到增強,其語法形式如下:

var singleton = function(){
    // 私有變數和私有函式
    var privateVar = 10;
    function privateFunc(){
        return (privateVar %2 === 0);
    }
    // 特權 / 公有方法和屬性
    return {
        publicProperty : true,
        publicMethod : function() {
            privateVar++;
            return privateFunc();
        }
    };
}();

這個模組模式使用了一個返回物件的匿名函式。在這個匿名函式內部,首先定義了私有變數和函式。然後,將一個物件字面量作為函式的值返回。返回的物件字面量中只包含可以公開的屬性和方法。由於這個物件是在匿名函式內部定義的,因此它的公有方法有權訪問私有變數和函式。從本質上來講,這個物件字面量定義的是單例的公共介面。這種模式在需要對單例進行某些初始化,同時又需要維護其私有變數時是非常有用的,例如:

var application = function(){
    // 私有變數和函式
    var componets = [];
    // 初始化
    components.push(new BaseComponent());
    // 公共
    return {
        getComponentCount: function(){
            return components.length;
        }
        registerComponent: function(component){
            if (typeof component == 'object'){
                components.push(component);
            }
        }
    };
}();

Web應用程式中,經常需要使用一個單例來管理應用程式級的資訊。這個簡單的例子建立了一個用於管理元件的application物件。在建立這個物件的過程中,首先宣告瞭一個私有的components陣列,並向陣列中新增了一個BaseComponent的新例項。(這裡只是一個例項,沒有具體的程式碼)而返回物件的getComponents()registerComponent()方法,都是有權訪問陣列components的特權方法。前者只是返回已註冊的元件數目,後者用於註冊新元件。

簡而言之,如果必須建立一個物件並以某些資料對其進行初始化,同時還要公開一些能夠訪問這些私有資料的方法,那麼久可以使用模組模式。以這種模式建立的每個單例都是Object的例項,因為最終要通過一個物件字面量來表示它。事實上,這也沒什麼;畢竟,單例通常是作為全域性物件存在的,我們不會將它傳遞給一個函式。因此,也就沒有什麼必要使用instanceof操作符來檢查其物件型別了

4.3 增強的模組模式

有人進一步改進了模組模式,即在返回物件之前加入對其增強的程式碼。這種增強的模組模式適合那些單例必須是某種型別的例項,同時還必須新增某些屬性和(或)方法對其進行增強的情況。如下:

var singleton = function(){
    var privateVar = 10;
    function privateFunc(){
        return privateVar %2 === 0;
    }
    var obj = new CustomType();
    obj.publicProperty = true;
    obj.publicMethod = function(){
        privateVar++;
        return privateFunc();
    }

    return obj;
}();

如果前面演示模組模式的例子中的application物件必須是BaseComponent的例項,那麼就可以使用下面的程式碼。

var application = function(){
    // 私有變數和函式
    var components = [];
    // 初始化
    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;
}();

在這個重寫後的應用程式(application)單例中,首先也是像前面的例子中一樣定義了私有變數。主要的不同之處在於命名變數app的建立過程,因為它必須是BaseComponent的例項。這個實際上是application物件的區域性變數版。此後,我們又為app物件新增了能夠訪問私有變數的公有方法。最後一步是返回app物件,結果仍然是將它賦值給全域性變數application

5 小結

Javascript程式設計中,函式表示式是一種非常有用的技術,使用函式表示式可以無需對函式命令,從而實現動態程式設計。匿名函式,是一種使用Javascript函式的強大方式。以下總結了函式表示式的特點。

  • 函式表示式不同於函式宣告。函式宣告要求有名字,但函式表示式不需要。沒有名字的函式表示式也叫做匿名函式。
  • 在無法確定如何引用函式的情況下,遞迴函式就會變得比較複雜;
  • 遞迴函式應該使用使用arguments.callee來遞迴呼叫自身,不要使用函式名——函式名可能會發生變化。(嚴格模式下,不能使用arguments.callee)

當在函式內部定義了其他函式時,就建立了閉包。閉包有權訪問包含函式內部的所有變數,原理如下。

  • 在後臺執行環境中,閉包的作用域包含它自己的作用域,包含函式的作用域和全域性的作用域。
  • 通常,函式的作用域及其所有變數都會在函式執行結束後被銷燬。
  • 但是,當函式返回了一個閉包時,這個函式的作用域將會一直在記憶體中保持到閉包不存在為止。

使用閉包可以在Javascript中模仿塊級作用域(Javascript中沒有塊級作用域的概念)。要點如下.

  • 即使Javascript中沒有正式的私有物件屬性的概念,但可以使用閉包來實現公有方法,而通過公有方法可以訪問在包含作用域中定義的變數。
  • 有權訪問私有變數的公有方法叫做特權方法。
  • 可以使用建構函式模式(導致記憶體浪費),原型模式(導致私有變數被全部例項共享)來實現自定義型別的特權方法,也可以使用模組模式,增強模式來實現單例的特權方法。

Javascript中的函式表示式和閉包都是極其有用的特性,利用它們可以實現很多功能。不過因為建立閉包必須維護額外的作用域(系統自己維護),所有過度使用它們可能會佔用大量記憶體。

相關文章