JavaScript設計模式

1.弱類型語言

  • 在JavaScript中,定義變量時沒必要聲明其類型。但這並不意味着變量沒有類型。一個變量能夠屬於幾種類型之一,這取決於其包含的數據。JavaScript中有三種原始類型:布爾型、數值型和字符串類型(不區分整數和浮點數是JavaScript與大多數其餘主流語言的一個不一樣之處)。此外,還有對象類型和包含可執行代碼的函數類型,前者是一種複合數據類型(數組是一種特殊的對象,它包含着一批值的有序集合)。最後,還有空類型(null)未定義類型(undefined)這兩種數據類型。原始數據類型按值傳送,而其餘數據類型則按引用傳送。
  • 與其餘弱類型語言同樣,JavaScript中的變量能夠根據所賦的值改變類型。原始類型之間也能夠進行類型轉換。toString能夠把數值或布爾值轉爲字符串。parseFloat和parseInt函數能夠把字符串轉變爲數值。雙重「非」能夠把字符串或數值轉變爲布爾值:var bool = !!num;

2.初談閉包

匿名函數最有趣的用途是用來建立閉包。閉包是一個受到保護的變量空間,由內嵌函數生成。JavaScript具備函數級的做用域。這意味着定義在函數內部的變量在函數外部不能被訪問。JavaScript的做用域又是詞法性質的。這意味着函數運行在定義它的做用域中,而不是在調用它的做用域中。把這兩個因素結合起來,就能經過把變量包裹在匿名函數中而對其加以保護。

3.依賴於接口的設計模式

下面列出的設計模式,尤爲依賴接口:程序員

  • 工廠模式。對象工廠所建立的具體對象會因具體狀況而異。使用接口能夠確保所建立出來的這些對象能夠互換使用。也就是說,對象工廠能夠保證其生產出來的對象都實現了必需的方法。
  • 組合模式。若是不用接口你就不可能用這個模式。組合模式的中心思想在於能夠將對象羣體與其組成對象同等對待。這是經過讓它們實現一樣的接口來作到的。若是不進行某種形式的鴨式辨型或類型檢查,組合模式就會失去大部分做用。
  • 裝飾者模式。裝飾者經過透明地爲另外一對象提供包裝而發揮做用。這是經過實現與另外那個對象徹底相同的接口而作到的。對於外界而言,一個裝飾者和它所包裝的對象看不出有什麼區別。
  • 命令模式。代碼中全部的命令對象都要實現同一批方法。經過使用接口,你爲執行這些命令對象而建立的類能夠沒必要知道這些對象具體是什麼,只要知道它們都實現了正確的接口便可。

4.用命名規範區別私用成員

在一些方法和屬性的名稱前面加下劃線以示其私用性。下劃線的這種用法是一個衆所周知的命名規範,它代表一個屬性(或方法)僅供對象內部使用,直接訪問它或設置它可能會致使意想不到的後果。這有助於防止程序員對它的無心使用,卻不能防止對它的有意使用。後一個目標的實現須要有真正私用性的方法。

5.做用域

下面這個示例說明了JavaScript中做用域的特色:編程

function foo() {
    var a = 10;

    function bar() {
        a *= 2;
    }

    bar();
    return a;
}

在這個示例中,a定義在函數foo中,但函數bar能夠訪問它,由於bar也定義在foo中。bar在執行過程當中將a設置爲a乘以2。當bar在foo中被調用時它可以訪問a,這能夠理解。可是若是bar是在foo外部被調用呢?設計模式

function foo() {
    var a = 10;

    function bar() {
        a *= 2;
        return a;
    }

    return bar;
}

var baz = foo();
console.log(baz());//20
console.log(baz());//40
console.log(baz());//80

var blat = foo();
console.log(blat());//20

在上述代碼中,所返回的對bar函數的引用被賦給變量baz。這個函數如今是在foo外部被調用,但它依然可以訪問a。這是由於JavaScript的做用域是詞法性的。函數是運行在定義它們的做用域中(本例中是foo內部的做用域),而不是運行在調用它們的做用域中。只要bar被定義在foo中,它就能訪問在foo中定義的全部變量,即便foo的執行已經結束。
這就是閉包的一個例子。在foo返回後,它的做用域被保存下來,但只有它返回的那個函數可以訪問這個做用域。在前面的示例中,baz和blat各有這個做用域及a的一個副本,並且只有它們本身能對其進行修改。返回一個內嵌函數是建立閉包最經常使用的手段。數組

6.用閉包實現私用成員的弊端

在門戶打開型對象建立模式中,全部方法都建立在原型對象中,所以無論派生多少對象實例,這些方法在內存中只存在一份。而包含特權方法、私用成員的建立模式中,每生成一個新的對象示例都將爲每個私用方法和特權方法生成一個新的副本。這會比其餘作法耗費更多內存,因此只宜用在須要真正的私用成員的場合。這種對象建立模式也不利於派生子類,由於所派生出的子類不能訪問超類的任何私用屬性或方法。相比之下,在大多數語言中,子類都能訪問超類的全部私有屬性和方法。故在JavaScript中用閉包實現私用成員致使的派生問題稱爲「繼承破壞封裝」。

7.靜態方法和屬性

前面所講的做用域和閉包的概念可用於建立靜態成員,包括公用和私用的。大多數方法和屬性所關聯的是類的實例,而靜態成員所關聯的則是類自己。換句話說,靜態成員是在累的層次上操做,而不是在實例的層次上操做。每一個靜態成員都只有一份。稍後將會看到,靜態成員是直接經過類對象訪問的。
下面是添加了靜態屬性和方法的Book類:
var Book = (function () {

    //私有靜態變量
    var numOfBooks = 0;

    //私有靜態方法
    function checkIsbn(isbn) {

    }

    //返回一個構造器
    return function (newIsbn, newTitle, newAuthor) {
        //私有屬性
        var isbn, title, author;

        //特權方法
        this.getIsbn = function () {
            return isbn;
        };
        this.setIsbn = function (newIsbn) {
            if (!checkIsbn(newIsbn)) {
                throw new Error('Book: Invalid ISBN.');
            }
            isbn = newIsbn;
        };
        this.getTitle = function () {
            return title;
        };
        this.setTitle = function (newTitle) {
            title = newTitle || "No title specified";
        };
        this.getAuthor = function () {
            return author;
        };
        this.setAuthor = function (newAuthor) {
            author = newAuthor || "No author specified";
        };

        //Constructed code.
        numOfBooks++;
        if (numOfBooks > 50) {
            throw new Error(".");
        }
        this.setIsbn(newIsbn);
        this.setTitle(newTitle);
        this.setAuthor(newAuthor);
    }
})();

//公共靜態方法
Book.convertToTitleCase = function (inputString) {

};

//公共非特權方法
Book.prototype = {
    display: function () {

    }
};

這裏的私用成員和特權成員仍然被聲明在構造器中(分別使用var和this關鍵字)。但哪一個構造器卻從原來的普通函數變成了一個內嵌函數,而且被做爲包含它的函數的返回值賦給變量Book。這就建立了一個閉包,你能夠把靜態的私用成員聲明在裏面。位於外層函數聲明以後的一對空括號很重要,其做用是一段代碼載入就當即執行這個函數(而不是在調用Book構造函數時)。這個函數的返回值是另外一個函數,它被賦給Book變量,Book所以成了一個構造函數。在實例化Book時,所調用的是這個內層函數。外層那個函數只是用於建立一個能夠用來存放靜態私用成員的閉包。瀏覽器

  • 在本例中,checkIsbn被設計爲靜態方法 ,緣由是爲Book的每一個實例都生成這個方法的一個新副本毫無道理。此外還有一個靜態屬性numOfBooks,其做用在於跟蹤Book構造器的總調用次數。本例利用這個屬性將Book實例的個數限制爲不超過50個。
  • 這些私用的靜態成員能夠從構造器內部訪問,這意味着全部私用函數和特權函數都能訪問它們。與其餘方法相比,它們有一個明顯的優勢,那就是內存中只會存放一份。由於其中那些靜態方法被聲明在構造器以外,因此它們不是特權方法,不能訪問任何定義在構造器中的私用屬性。定義在構造器中的私用方法可以調用那些私用靜態方法,反之則否則。要判斷一個私用方法是否應該被設計爲靜態方法,一條經驗法則是看它是否須要訪問任何實例數據。若是它不須要,那麼將其設計爲靜態方法會更有效率(從內存佔用的意義上來說),由於它只會被建立一份。
  • 建立公用的靜態成員則容易得多,只需直接將其做爲構造函數這個對象的屬性建立便可,前述代碼中的方法converToTitleCase就是一例。這實際上至關於把構造器做爲命名空間來使用。
  • 全部公用靜態方法若是做爲獨立的函數來聲明其實也一樣簡單,但最好仍是像這樣把相關行爲集中在一塊兒。這些方法用於與類這個總體相關的任務,而不是與類的任一特定實例相關的任務。它們並不直接依賴於對象實例中包含的任何數據。

8.私用變量模仿常量

經過建立只有取值器而沒有賦值器的私用變量能夠模仿常量。
var Class = (function () {
    var UPPER_BOUND = 100;

    //構造器
    var ctor = function (constructorArgument) {

    };
    //靜態特權方法
    ctor.getUPPER_BOUND = function () {
        return UPPER_BOUND;
    };
    return ctor;

})();

9.封裝之弊

  • 私用方法很難進行單元測試。由於它們及其內部變量都是私用的,因此在對象外部沒法訪問到它們。這個問題沒有什麼很好的應對之策。你要麼經過使用公用方法來提供訪問途徑(這樣一來就葬送了使用私有方法所帶來的大多數好處),要麼設法在對象內部定義並執行全部單元測試。最好的解決辦法是隻對公用方法進行單元測試。這應該能覆蓋到全部私用方法,儘管對它們的測試只是間接的。這種問題不是JavaScript所獨有的,只對公用方法進行單元測試是一種廣爲接收的處理方式。
  • 使用封裝意味着不得不與複雜的做用域鏈打交道。
  • 封裝可能會損害類的靈活性,導致其沒法被用於某些你不曾想到過的目的。

10.單體模式

單體模式是JavaScript中最基本但又最有用的模式之一,它可能比其餘任何模式都更經常使用。這種模式提供了一種將代碼組織爲一個邏輯單元的手段,這個邏輯單元中的代碼能夠經過單一的變量進行訪問。經過確保單體對象只存在一份實例,你就能夠確信本身的全部代碼使用的都是一樣的全局資源。
單體類在JavaScript中有許多用處。它們能夠用來劃分命名空間,以減小網頁中全局變量的數目。更重要的是,藉助於單體模式,你能夠把代碼組織得更爲一致,從而使其更容易閱讀和維護。

11.單體的基本結構

var Singleton = {
    attribute1: true,
    attribute2: 10,
    method1: function () {

    },
    method2: function (args) {

    }
};
  • 這個單體對象能夠被修改。你能夠爲其添加新成員,這一點與別的對象字面量沒有什麼不一樣。你也能夠用delete運算符刪除其現有成員。這實際上違背了面向對象設計的一條原則:類能夠被擴展,但不該該被修改。
  • 按傳統的定義,單體是一個只能被實例化一次而且能夠經過一個衆所周知的訪問點訪問的類。要是嚴格按照這個定義來講,前面的例子所示的並非一個單體,由於它不是一個可實例化的類。咱們打算把單體模式定義的更廣義一些:單體是一個用來劃分命名空間並將一批相關方法和屬性組織在一塊兒的對象,若是能夠被實例化,那麼它只能被實例化一次。

12.劃分命名空間

爲了不無心中改寫變量,最好的解決辦法之一是用單體對象將代碼組織在命名空間之中。下面是前面的例子用單體模式改良後的結果:數據結構

var MyNamespace = {
    findProduct: function (id) {

    }
};

如今findProduct函數是MyNamespace中的一個方法,它不會被全局命名空間中聲明的任何新變量改寫。要注意,該方法仍然能夠從各個地方訪問。不一樣之處在於如今其調用方式不是findProduct(id),而是MyNamespace.findProduct(id)。還有一個好處就是,這可讓其餘程序員大致知道這個方法的聲明地點及其做用。用命名空間把相似的方法組織到一塊兒,也有助於加強代碼的文檔性。閉包

13.模塊模式

有一種單體模式被稱爲模塊模式,由於它能夠把一批相關方法和屬性組織爲模塊並起到劃分命名空間的做用。例如:
MyNamespace.Singleton = (function () {
    //私有成員
    var privateAttribute1 = false;
    var privateAttribute2 = [1, 2, 3];

    function privateMethod1() {

    }

    function privateMethod2() {

    }

    return {
        //public members
        publicAttribute1: true,
        publicAttribute2: 10,
        publicMethod1: function () {

        },
        publicMethod2: function (args) {

        }
    }
})();

14.簡單工廠模式

最好用一個例子來講明簡單工廠模式的概念。假設你想開幾個自行車商店,每一個店都有幾種型號的自行車出售。這能夠用一個類來表示:
/*BicycleShop class.*/
var BicycleShop = function () {

};

BicycleShop.prototype = {
    sellBicycle: function (model) {
        var bicycle;
        switch (model) {
            case "The Speedster":
                bicycle = new SpeedSter();
                break;
            case "The Lowrider":
                bicycle = new Lowrider();
                break;
            case "The Comfort Cruiser":
            default:
                bicycle = new ComfortCruiser();
        }
        Interface.ensureImplements(bicycle, Bicycle);
        bicycle.assemble();
        bicycle.wash();
        return bicycle;
    }
};
sellBicycle方法根據所要求的自行車型號用switch語句建立一個自行車的實例。各類型號的自行車實例能夠互換使用,由於它們都實現了Bicycle接口:
/* The Bicycle interface. */
var Bicycle = new Interface('Bicycle', ['assemble', 'wash', 'ride', 'repair']);

/* Speedster class. */
var Speedster = function () {

};
Speedster.prototype = {
    assemble: function () {
    },
    wash: function () {
    },
    ride: function () {
    },
    repair: function () {
    }
};
要出售某種型號的自行車,只要調用sellBicycle方法便可:
var californiaCruisers = new BicycleShop();
var yourNewBike = californiaCruisers.sellBicycle("The Speedster");
在狀況發生變化以前,這倒也挺管用。但要是你想在供貨目錄中加入一款新車型又會怎麼樣呢?你得爲此修改BicycleShop的代碼,哪怕這個類的實際功能實際上並無發生改變——依舊是建立一個自行車的新實例,組裝它,清洗它,而後把它交給顧客。更好的解決辦法是把sellBicycle方法中「建立新實例」這部分工做轉交給一個簡單工廠對象:
/* BicycleFactory namespace. */
var BicycleFactory = {
    createBicycle:function(model){
        var bicycle;
        switch (model) {
            case "The Speedster":
                bicycle = new SpeedSter();
                break;
            case "The Lowrider":
                bicycle = new Lowrider();
                break;
            case "The Comfort Cruiser":
            default:
                bicycle = new ComfortCruiser();
        }
        Interface.ensureImplements(bicycle, Bicycle);
        return bicycle;
    }
};

BicycleFactory是一個單體,用來把createBicycle方法封裝在一個命名空間中。這個方法返回一個實現了Bicycle接口的對象,而後你能夠照常對其進行組裝和清洗:async

/* BicycleShop class, improved. */
var BicycleShop = function () {
};
BicycleShop.prototype = {
    sellBicycle: function (model) {
        var bicycle = BicycleFactory.createBicycle(model);
        bicycle.assemble();
        bicycle.wash();
        return bicycle;
    }
};

這個BicycleFactory對象能夠供各類類用來建立新的自行車實例。有關可供車型的全部信息集中在一個地方管理 ,因此添加更多車型很容易:ide

/* BicycleFactory namespace,with more models. */
var BicycleFactory = {
    createBicycle: function (model) {
        var bicycle;
        switch (model) {
            case "The Speedster":
                bicycle = new SpeedSter();
                break;
            case "The Lowrider":
                bicycle = new Lowrider();
                break;
            case "The Flatlander":
                bicycle = new Flatlander();
                break;
            case "The Comfort Cruiser":
            default:
                bicycle = new ComfortCruiser();
        }
        Interface.ensureImplements(bicycle, Bicycle);
        return bicycle;
    }
};

15.工廠模式

真正的工廠模式與簡單工廠模式的區別在於,它不是另外使用一個類或對象來建立自行車,而是使用一個子類。按照正式定義,工廠是一個將其成員對象的實例化推遲到子類中進行的類。

16.工廠模式的適用場合

  • 動態實現:若是須要建立一些用不一樣方式實現同一接口的對象,那麼可使用一個工廠方法或簡單工廠對象來簡化選擇實現的過程。
  • 節省設置開銷:若是對象須要進行復雜而且彼此相關的設置,那麼使用工廠模式能夠減小每種對象所需的代碼量。若是這種設置只須要爲特定類型的全部實例執行一次便可,這種做用尤其突出。把這種設置代碼放到類的構造函數中並非一種高效的作法,這是由於即使設置工做已經完成,每次建立新實例的時候這些代碼仍是會執行,並且這樣作會把設置代碼分散到不一樣的類中。工廠方法很是適合於這種場合。它能夠在實例化全部須要的對象以前先一次性地進行設置。不管有多少類會被實例化,這種辦法均可以讓設置代碼集中在一個地方。
  • 用許多小型對象組成一個大對象

17.工廠模式之利

  • 工廠模式的主要好處在於消除對象間的耦合。經過使用工廠方法而不是new關鍵字及具體類,你能夠把全部實例化的代碼集中在一個位置。這能夠大大簡化更換所用的類或在運行期間動態選擇所用的類的工做。在派生子類時它也提供了更強大的靈活性。
  • 全部這些好處都與面向對象設計的這兩條原則有關:弱化對象間的耦合;防止代碼的重複。在一個方法中進行類的實例化,能夠消除重複性的代碼。這是在用一個對接口的調用取代一個具體的實現。這些都有助於建立模塊化的代碼。

18.橋接模式

橋接模式最多見和實際的應用場合之一就是事件監聽器回調函數。假設有一個名爲getBeerById的API函數,它根據一個標識符返回有關某種啤酒的信息。你但願用戶在點擊的時候獲取這種信息。那個被點擊的元素極可能有啤酒的標識符信息,它多是做爲元素自身的ID保存,也多是做爲別的自定義屬性保存。下面是一種作法:模塊化

addEvent(element, 'click', getBeerById);
function getBeerById(e) {
    var id = this.id;
    asyncRequest('GET', 'beer.uri?id=' + id, function (resp) {
        console.log(resp.responseText);
    });
}

這個API只能工做在瀏覽器中,若是要對這個API函數作單元測試,或者在命令行中執行,可能會報錯。一個優良的API設計,不該該把它與任何特定的實現攪在一塊兒。

function getBeerById(id, callback) {
    asyncRequest('GET', 'beer.uri?id=' + id, function (resp) {
        callback(resp.responseText);
    })
}

如今咱們將針對接口而不是實現進行編程,用橋接模式把抽象隔離開來:

addEvent(element, 'click', getBeerByIdBridge);
function getBeerBIdBridge(e) {
    getBeerById(this.id, function (beer) {
        console.log(beer);
    });
}

這下getBeerById並無和事件對象捆綁在一塊兒了。

19.用橋接模式聯結多個類

var Class1 = function (a, b, c) {
    this.a = a;
    this.b = b;
    this.c = c;
};
var Class2 = function (d) {
    this.d = d;
};
var BridgeClass = function (a, b, c, d) {
    this.one = new Class1(a, b, c);
    this.two = new Class2(d);
};

20.適配器模式

適配器模式能夠用來在現有接口和不兼容的類之間進行適配。使用這種模式的對象又叫包裝器,由於它們是在用一個新的接口包裝另外一個對象。

21.適配器的特色

  • 適配器能夠被添加到現有代碼中以協調兩個不一樣的接口。若是現有代碼的接口能很好地知足須要,那就可能沒有必要使用適配器。
  • 從表面上看,適配器模式很像門面模式。它們都要對別的對象進行包裝並改變其呈現的接口。兩者的差異在於它們如何改變接口。門面元素展示的是一個簡化的接口,它並不提供額外的選擇,並且有時爲了方便完成某些常見任務它還會作出一些假定。而適配器則要把一個接口轉換爲另外一個接口,它並不會濾除某些能力,也不會簡化接口。若是客戶系統期待的API不可用,那就須要用到適配器。
  • 適配器可被實現爲不兼容的方法調用之間的一個代碼薄層。
  • 示例:
  • 假如你有一個對象還有一個以三個字符串爲參數的函數:
var clientObject = {
        string1: "foo",
        string2: "bar",
        string3: "baz"
    };
    function interfaceMethod(str1, str2, str3) {

    }

爲了把clientObject做爲參數傳遞給interfaceMethod,須要用到適配器。咱們能夠這樣建立一個:

function clientToInterfaceAdapter(o) {
    interfaceMethod(o.string1, o.string2, o.string3);
}
//如今就能夠把整個對象傳給這個函數
clientToInterfaceAdapter(clientObject);

clientToInterfaceAdapter函數的做用就在於對interfaceMethod函數進行包裝,並把傳遞給它的參數轉換給後者須要的形式。

22.裝飾者模式

裝飾者模式可用來透明地把對象包裝在具備一樣接口的另外一對象中。這樣一來,你能夠給一個方法添加一些行爲,而後將方法調用傳遞給原始對象。相對於建立子類來講,使用裝飾者對象是一種更靈活的選擇。

23.享元模式

享元模式最適合於解決因建立大量相似對象而累及的性能問題。這種模式在JavaScript中尤爲有用,由於複雜的JavaScript代碼可能很快就會用光瀏覽器的全部可用內存。經過把大量獨立對象轉化爲少許共享對象,能夠下降運行Web應用程序所需的資源數量。

享元模式用於減小應用程序所需對象的數量。這是經過將對象的內部狀態劃分爲內在數據和外在數據兩類而實現的。內在數據是指類的內部方法所需的信息,沒有這種數據的話類不能正常運轉。外在數據則是能夠從類身上剝離並存儲在其外部的信息。咱們能夠將內在狀態相同的全部對象替換爲同一個共享對象,這種方法能夠把對象數量減小到不一樣內在狀態的數量。

24.實現享元模式的通常步驟

  1. 將全部外在數據從目標剝離。具體作法是儘量多地刪除該類的屬性,所刪除的應該是那種因實例而異的屬性。構造函數的參數也要這樣處理。這些參數應該被添加到該類的各個方法。這些外在數據如今再也不保存在類的內部,而是由管理器提供給類的方法。通過這樣的處理後,目標類應該依然具備與以前同樣的功能。惟一的區別在於數據的來源發生了變化。
  2. 建立一個用來控制該類的實例化的工廠。這個工廠應該掌握該類全部已建立出來的獨一無二的實例。其具體作法之一是用一個對象字面量來保存每個這類對象的引用,並以用來生成這些對象的參數的惟一性組合做爲它們的索引。這樣一來,每次要求工廠提供一個對象時,它會先檢查那個對象字面量,看看之前是否請求過這個對象。若是是,那麼只要返回那個現有對象的引用就行。不然它會建立一個新對象並將其引用保存在那個對象字面量中,而後返回這個對象。另外一種作法稱爲對象池,這種技術用數組來保存所建立的對象的引用。它適合於注重可用對象的數量而不是那些單獨配置的實例的場合。這種技術可用來將所實例化的對象的數目維持在最低值。工廠會處理根據內在數據建立對象的全部事宜。
  3. 建立一個用來保存外在數據的管理器。該管理器對象負責控制處理外在數據的種種事宜。在實施優化以前,要是須要一個目標類的實例,你會把全部數據傳給構造函數以建立其新實例。而如今要是須要一個實例,你會調用管理器的某個方法,把全部數據都提供給它。這個方法會分辨內在數據和外在數據。它把內在數據提供給工廠對象以建立一個對象(或者,若是已經存在這樣一個對象的話,則重用該對象)。外在數據則被保存在管理器內的一個數據結構中。管理器隨後會根據須要將這些數據提供給共享對象的方法,其效果就如同該類有許多實例同樣。

25.觀察者模式

  • 在事件驅動的環境中,好比瀏覽器這種持續尋求用戶關注的環境中,觀察者模式(又名發佈者-訂閱者模式)是一種管理人與其任務之間的關係(確切的說,是對象及其行爲和狀態之間的關係)的得力工具。
  • 觀察者模式中存在兩個角色:觀察者和被觀察者。
相關文章
相關標籤/搜索