1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        JavaScript設(shè)計模式總匯

        共 41527字,需瀏覽 84分鐘

         ·

        2021-01-15 08:52

        來源 |?http://www.fly63.com/article/detial/10063


        設(shè)計模式簡介:

        設(shè)計模式是可重用的用于解決軟件設(shè)計中一般問題的方案。設(shè)計模式如此讓人著迷,以至在任何編程語言中都有對其進(jìn)行的探索。
        其中一個原因是它可以讓我們站在巨人的肩膀上,獲得前人所有的經(jīng)驗,保證我們以優(yōu)雅的方式組織我們的代碼,滿足我們解決問題所需要的條件。
        設(shè)計模式同樣也為我們描述問題提供了通用的詞匯。這比我們通過代碼來向別人傳達(dá)語法和語義性的描述更為方便。
        下面介紹一些JavaScript里用到的設(shè)計模式:

        1、構(gòu)造器模式

        在面向?qū)ο缶幊讨校瑯?gòu)造器是一個當(dāng)新建對象的內(nèi)存被分配后,用來初始化該對象的一個特殊函數(shù)。在JavaScript中幾乎所有的東西都是對象,我們經(jīng)常會對對象的構(gòu)造器十分感興趣。
        對象構(gòu)造器是被用來創(chuàng)建特殊類型的對象的,首先它要準(zhǔn)備使用的對象,其次在對象初次被創(chuàng)建時,通過接收參數(shù),構(gòu)造器要用來對成員的屬性和方法進(jìn)行賦值。

        1.1創(chuàng)建對象

        // 第一種方式let obj = {};// 第二種方式let obj2 = Object.create( null );// 第三種方式let obj3 = new Object();

        1.2設(shè)置對象的屬性和方法

        // 1. “點(diǎn)號”法// 設(shè)置屬性obj.firstKey = "Hello World";// 獲取屬性let key = obj.firstKey;// 2. “方括號”法// 設(shè)置屬性obj["firstKey"] = "Hello World";// 獲取屬性let key = newObject["firstKey"];// 方法1和2的區(qū)別在于用方括號的方式內(nèi)可以寫表達(dá)式// 3. Object.defineProperty方式// 設(shè)置屬性Object.defineProperty(obj, "firstKey", {    value: "hello world",// 屬性的值,默認(rèn)為undefined    writable: true, // 是否可修改,默認(rèn)為false    enumerable: true,// 是否可枚舉(遍歷),默認(rèn)為false    configurable: true // 表示對象的屬性是否可以被刪除,以及除 value 和 writable 特性外的其他特性是否可以被修改。});// 如果上面的方式你感到難以閱讀,可以簡短的寫成下面這樣:let defineProp = function ( obj, key, value ){  let config = {};  config.value = value;  Object.defineProperty( obj, key, config );};// 4. Object.defineProperties方式(同時設(shè)置多個屬性)// 設(shè)置屬性Object.defineProperties( obj, {  "firstKey": {     value: "Hello World",     writable: true   },  "secondKey": {     value: "Hello World2",     writable: false   }});

        1.3創(chuàng)建構(gòu)造器

        Javascript不支持類的概念,但它有一種與對象一起工作的構(gòu)造器函數(shù)。使用new關(guān)鍵字來調(diào)用該函數(shù),我們可以告訴Javascript把這個函數(shù)當(dāng)做一個構(gòu)造器來用,它可以用自己所定義的成員來初始化一個對象。

        在這個構(gòu)造器內(nèi)部,關(guān)鍵字this引用到剛被創(chuàng)建的對象?;氐綄ο髣?chuàng)建,一個基本的構(gòu)造函數(shù)看起來像這樣:

        function Car( model, year, miles ) {  this.model = model;  this.year = year;  this.miles = miles;  this.toString = function () {    return this.model + " has done " + this.miles + " miles";  };}// 使用:// 我們可以示例化一個Carlet civic = new Car( "Honda Civic", 2009, 20000 );let mondeo = new Car( "Ford Mondeo", 2010, 5000 );// 打開瀏覽器控制臺查看這些對象toString()方法的輸出值// output of the toString() method being called on// these objectsconsole.log( civic.toString() );console.log( mondeo.toString() );

        上面是簡單版本的構(gòu)造器模式,但它還是有些問題。一個是難以繼承,另一個是每個Car構(gòu)造函數(shù)創(chuàng)建的對象中,toString()之類的函數(shù)都被重新定義。這不是非常好,理想的情況是所有Car類型的對象都應(yīng)該引用同一個函數(shù)。?

        在Javascript中函數(shù)有一個prototype的屬性。當(dāng)我們調(diào)用Javascript的構(gòu)造器創(chuàng)建一個對象時,構(gòu)造函數(shù)prototype上的屬性對于所創(chuàng)建的對象來說都看見。照這樣,就可以創(chuàng)建多個訪問相同prototype的Car對象了。下面,我們來擴(kuò)展一下原來的例子:

        function Car( model, year, miles ) {  this.model = model;  this.year = year;  this.miles = miles;}Car.prototype.toString = function () {  return this.model + " has done " + this.miles + " miles";};// 使用:var civic = new Car( "Honda Civic", 2009, 20000 );var mondeo = new Car( "Ford Mondeo", 2010, 5000 );console.log( civic.toString() );console.log( mondeo.toString() );

        通過上面代碼,單個toString()實例被所有的Car對象所共享了。

        2、模塊化模式

        模塊是任何健壯的應(yīng)用程序體系結(jié)構(gòu)不可或缺的一部分,特點(diǎn)是有助于保持應(yīng)用項目的代碼單元既能清晰地分離又有組織。

        在JavaScript中,實現(xiàn)模塊有幾個選項,他們包括:

        • 模塊化模式

        • 對象表示法

        • AMD模塊

        • Commonjs?模塊

        • ECMAScript Harmony 模塊

        2.1對象字面值

        對象字面值不要求使用新的操作實例,但是不能夠在結(jié)構(gòu)體開始使用,因為打開"{"可能被解釋為一個塊的開始。

        let myModule = {  myProperty: "someValue",  // 對象字面值包含了屬性和方法(properties and methods).  // 例如,我們可以定義一個模塊配置進(jìn)對象:  myConfig: {    useCaching: true,    language: "en"  },  // 非?;镜姆椒?/span>  myMethod: function () {    console.log( "Where in the world is Paul Irish today?" );  },  // 輸出基于當(dāng)前配置configuration的一個值  myMethod2: function () {    console.log( "Caching is:" + ( this.myConfig.useCaching ) ? "enabled" : "disabled" );  },  // 重寫當(dāng)前的配置(configuration)  myMethod3: function( newConfig ) {    if ( typeof newConfig === "object" ) {      this.myConfig = newConfig;      console.log( this.myConfig.language );    }  }};myModule.myMethod();// Where in the world is Paul Irish today?myModule.myMethod2();// enabledmyModule.myMethod3({  language: "fr",  useCaching: false});// fr

        2.2模塊化模式

        模塊化模式最初被定義為一種對傳統(tǒng)軟件工程中的類提供私有和公共封裝的方法。

        在JavaScript中,模塊化模式用來進(jìn)一步模擬類的概念,通過這樣一種方式:我們可以在一個單一的對象中包含公共/私有的方法和變量,從而從全局范圍中屏蔽特定的部分。

        這個結(jié)果是可以減少我們的函數(shù)名稱與在頁面中其他腳本區(qū)域定義的函數(shù)名稱沖突的可能性。

        模塊模式使用閉包的方式來將"私有信息",狀態(tài)和組織結(jié)構(gòu)封裝起來。提供了一種將公有和私有方法,變量封裝混合在一起的方式,這種方式防止內(nèi)部信息泄露到全局中,從而避免了和其它開發(fā)者接口發(fā)生沖圖的可能性。

        在這種模式下只有公有的API 會返回,其它將全部保留在閉包的私有空間中。

        這種方法提供了一個比較清晰的解決方案,在只暴露一個接口供其它部分使用的情況下,將執(zhí)行繁重任務(wù)的邏輯保護(hù)起來。這個模式非常類似于立即調(diào)用函數(shù)式表達(dá)式(IIFE-查看命名空間相關(guān)章節(jié)獲取更多信息),但是這種模式返回的是對象,而立即調(diào)用函數(shù)表達(dá)式返回的是一個函數(shù)。

        需要注意的是,在javascript事實上沒有一個顯式的真正意義上的"私有性"概念,因為與傳統(tǒng)語言不同,javascript沒有訪問修飾符。從技術(shù)上講,變量不能被聲明為公有的或者私有的,因此我們使用函數(shù)域的方式去模擬這個概念。

        在模塊模式中,因為閉包的緣故,聲明的變量或者方法只在模塊內(nèi)部有效。在返回對象中定義的變量或者方法可以供任何人使用。

        let testModule = (function () {  let counter = 0;  return {    incrementCounter: function () {      return counter++;    },    resetCounter: function () {      console.log( "counter value prior to reset: " + counter );      counter = 0;    }  };})();testModule.incrementCounter();testModule.resetCounter();

        在這里我們看到,其它部分的代碼不能直接訪問我們的incrementCounter() 或者 resetCounter()的值。counter變量被完全從全局域中隔離起來了,因此其表現(xiàn)的就像一個私有變量一樣,它的存在只局限于模塊的閉包內(nèi)部,因此只有兩個函數(shù)可以訪問counter。

        我們的方法是有名字空間限制的,因此在我們代碼的測試部分,我們需要給所有函數(shù)調(diào)用前面加上模塊的名字(例如"testModule")。

        當(dāng)使用模塊模式時,我們會發(fā)現(xiàn)通過使用簡單的模板,對于開始使用模塊模式非常有用。下面是一個模板包含了命名空間,公共變量和私有變量。

        let myNamespace = (function () {  let myPrivateVar, myPrivateMethod;  myPrivateVar = 0;  myPrivateMethod = function( foo ) {      console.log( foo );  };  return {    myPublicVar: "foo",    myPublicFunction: function( bar ) {      myPrivateVar++;      myPrivateMethod( bar );    }  };})();

        看一下另外一個例子,下面我們看到一個使用這種模式實現(xiàn)的購物車。這個模塊完全自包含在一個叫做basketModule 全局變量中。

        模塊中的購物車數(shù)組是私有的,應(yīng)用的其它部分不能直接讀取。只存在與模塊的閉包中,因此只有可以訪問其域的方法可以訪問這個變量。

        let basketModule = (function () {  let basket = [];  function doSomethingPrivate() {    //...  }  function doSomethingElsePrivate() {    //...  }  return {    addItem: function( values ) {      basket.push(values);    },    getItemCount: function () {      return basket.length;    },    doSomething: doSomethingPrivate,    getTotal: function () {      let q = this.getItemCount(),          p = 0;      while (q--) {        p += basket[q].price;      }      return p;    }  };}());

        上面的方法都處于basketModule 的名字空間中。

        請注意在上面的basket模塊中 域函數(shù)是如何在我們所有的函數(shù)中被封裝起來的,以及我們?nèi)绾瘟⒓凑{(diào)用這個域函數(shù),并且將返回值保存下來。這種方式有以下的優(yōu)勢:

        • 可以創(chuàng)建只能被我們模塊訪問的私有函數(shù)。這些函數(shù)沒有暴露出來(只有一些API是暴露出來的),它們被認(rèn)為是完全私有的。

        • 當(dāng)我們在一個調(diào)試器中,需要發(fā)現(xiàn)哪個函數(shù)拋出異常的時候,可以很容易的看到調(diào)用棧,因為這些函數(shù)是正常聲明的并且是命名的函數(shù)。

        • 這種模式同樣可以讓我們在不同的情況下返回不同的函數(shù)。我見過有開發(fā)者使用這種技巧用于執(zhí)行測試,目的是為了在他們的模塊里面針對IE專門提供一條代碼路徑,但是現(xiàn)在我們也可以簡單的使用特征檢測達(dá)到相同的目的。

        2.3Import mixins(導(dǎo)入混合)

        這個變體展示了如何將全局(例如 jQuery, Underscore)作為一個參數(shù)傳入模塊的匿名函數(shù)。這種方式允許我們導(dǎo)入全局,并且按照我們的想法在本地為這些全局起一個別名。

        let myModule = (function ( jQ, _ ) {    function privateMethod1(){        jQ(".container").html("test");    }    function privateMethod2(){      console.log( _.min([10, 5, 100, 2, 1000]) );    }    return{        publicMethod: function(){            privateMethod1();                       }               };}( jQuery, _ ));// 將JQ和lodash導(dǎo)入myModule.publicMethod();

        2.4Exports(導(dǎo)出)

        這個變體允許我們聲明全局對象而不用使用它們。

        let myModule = (function () {  let module = {},    privateVariable = "Hello World";  function privateMethod() {    // ...  }  module.publicProperty = "Foobar";  module.publicMethod = function () {    console.log( privateVariable );  };  return module;}());

        2.5其它框架特定的模塊模式實現(xiàn)

        Dojo:

        Dojo提供了一個方便的方法 dojo.setObject() 來設(shè)置對象。這需要將以"."符號為第一個參數(shù)的分隔符,如:myObj.parent.child 是指定義在"myOjb"內(nèi)部的一個對象“parent”,它的一個屬性為"child"。

        使用setObject()方法允許我們設(shè)置children 的值,可以創(chuàng)建路徑傳遞過程中的任何對象即使這些它們根本不存在。

        例如,如果我們聲明商店命名空間的對象basket.coreas,可以使用如下方式:

        let store = window.store || {};
        if ( !store["basket"] ) { store.basket = {};}
        if ( !store.basket["core"] ) { store.basket.core = {};}
        store.basket.core = { key:value,};

        Extjs:

        // create namespaceExt.namespace("myNameSpace");// create applicationmyNameSpace.app = function () {  // do NOT access DOM from here; elements don't exist yet  // private variables  let btn1,      privVar1 = 11;  // private functions  let btn1Handler = function ( button, event ) {      console.log( "privVar1=" + privVar1 );      console.log( "this.btn1Text=" + this.btn1Text );    };  // public space  return {    // public properties, e.g. strings to translate    btn1Text: "Button 1",    // public methods    init: function () {      if ( Ext.Ext2 ) {        btn1 = new Ext.Button({          renderTo: "btn1-ct",          text: this.btn1Text,          handler: btn1Handler        });      } else {        btn1 = new Ext.Button( "btn1-ct", {          text: this.btn1Text,          handler: btn1Handler        });      }    }  };}();

        jQuery:

        因為jQuery編碼規(guī)范沒有規(guī)定插件如何實現(xiàn)模塊模式,因此有很多種方式可以實現(xiàn)模塊模式。Ben Cherry 之間提供一種方案,因為模塊之間可能存在大量的共性,因此通過使用函數(shù)包裝器封裝模塊的定義。

        在下面的例子中,定義了一個library 函數(shù),這個函數(shù)聲明了一個新的庫,并且在新的庫(例如 模塊)創(chuàng)建的時候,自動將初始化函數(shù)綁定到document的ready上。

        function library( module ) {  $( function() {    if ( module.init ) {      module.init();    }  });  return module;}let myLibrary = library(function () {  return {    init: function () {      // module implementation    }  };}());

        優(yōu)點(diǎn):

        既然我們已經(jīng)看到單例模式很有用,為什么還是使用模塊模式呢?首先,對于有面向?qū)ο蟊尘暗拈_發(fā)者來講,至少從javascript語言上來講,模塊模式相對于真正的封裝概念更清晰。

        其次,模塊模式支持私有數(shù)據(jù)-因此,在模塊模式中,公共部分代碼可以訪問私有數(shù)據(jù),但是在模塊外部,不能訪問類的私有部分(沒開玩笑!感謝David Engfer 的玩笑)。

        缺點(diǎn):

        模塊模式的缺點(diǎn)是因為我們采用不同的方式訪問公有和私有成員,因此當(dāng)我們想要改變這些成員的可見性的時候,我們不得不在所有使用這些成員的地方修改代碼。

        我們也不能在對象之后添加的方法里面訪問這些私有變量。也就是說,很多情況下,模塊模式很有用,并且當(dāng)使用正確的時候,潛在地可以改善我們代碼的結(jié)構(gòu)。

        其它缺點(diǎn)包括不能為私有成員創(chuàng)建自動化的單元測試,以及在緊急修復(fù)bug時所帶來的額外的復(fù)雜性。根本沒有可能可以對私有成員打補(bǔ)丁。

        相反地,我們必須覆蓋所有的使用存在bug私有成員的公共方法。開發(fā)者不能簡單的擴(kuò)展私有成員,因此我們需要記得,私有成員并非它們表面上看上去那么具有擴(kuò)展性。

        3、單例模式

        單例模式之所以這么叫,是因為它限制一個類只能有一個實例化對象。經(jīng)典的實現(xiàn)方式是,創(chuàng)建一個類,這個類包含一個方法,這個方法在沒有對象存在的情況下,將會創(chuàng)建一個新的實例對象。如果對象存在,這個方法只是返回這個對象的引用。

        在JavaScript語言中, 單例服務(wù)作為一個從全局空間的代碼實現(xiàn)中隔離出來共享的資源空間是為了提供一個單獨(dú)的函數(shù)訪問指針。

        我們能像這樣實現(xiàn)一個單例:

        let mySingleton = (function () {  // Instance stores a reference to the Singleton  let instance;  function init() {    // 單例    // 私有方法和變量    function privateMethod(){        console.log( "I am private" );    }    let privateVariable = "Im also private";    let privateRandomNumber = Math.random();    return {      // 共有方法和變量      publicMethod: function () {        console.log( "The public can see me!" );      },      publicProperty: "I am also public",      getRandomNumber: function() {        return privateRandomNumber;      }    };  };  return {    // 如果存在獲取此單例實例,如果不存在創(chuàng)建一個單例實例    getInstance: function () {      if ( !instance ) {        instance = init();      }      return instance;    }  };})();
        let myBadSingleton = (function () { // 存儲單例實例的引用 var instance; function init() { // 單例 let privateRandomNumber = Math.random(); return { getRandomNumber: function() { return privateRandomNumber; } }; }; return { // 總是創(chuàng)建一個新的實例 getInstance: function () { instance = init(); return instance; } };})();
        // 使用:let singleA = mySingleton.getInstance();let singleB = mySingleton.getInstance();console.log( singleA.getRandomNumber() === singleB.getRandomNumber() ); // true
        let badSingleA = myBadSingleton.getInstance();let badSingleB = myBadSingleton.getInstance();console.log( badSingleA.getRandomNumber() !== badSingleB.getRandomNumber() ); // true

        創(chuàng)建一個全局訪問的單例實例 (通常通過 MySingleton.getInstance()) 因為我們不能(至少在靜態(tài)語言中) 直接調(diào)用 new MySingleton() 創(chuàng)建實例. 這在JavaScript語言中是不可能的。

        在四人幫(GoF)的書里面,單例模式的應(yīng)用描述如下:

        • 每個類只有一個實例,這個實例必須通過一個廣為人知的接口,來被客戶訪問。

        • 子類如果要擴(kuò)展這個唯一的實例,客戶可以不用修改代碼就能使用這個擴(kuò)展后的實例。

        關(guān)于第二點(diǎn),可以參考如下的實例,我們需要這樣編碼:

        mySingleton.getInstance = function(){  if ( this._instance == null ) {    if ( isFoo() ) {       this._instance = new FooSingleton();    } else {       this._instance = new BasicSingleton();    }  }  return this._instance;};

        在這里,getInstance 有點(diǎn)類似于工廠方法,我們不需要去更新每個訪問單例的代碼。FooSingleton可以是BasicSinglton的子類,并且實現(xiàn)了相同的接口。

        盡管單例模式有著合理的使用需求,但是通常當(dāng)我們發(fā)現(xiàn)自己需要在javascript使用它的時候,這是一種信號,表明我們可能需要去重新評估自己的設(shè)計。

        這通常表明系統(tǒng)中的模塊要么緊耦合要么邏輯過于分散在代碼庫的多個部分。單例模式更難測試,因為可能有多種多樣的問題出現(xiàn),例如隱藏的依賴關(guān)系,很難去創(chuàng)建多個實例,很難清理依賴關(guān)系,等等。

        4、觀察者模式

        觀察者模式是這樣一種設(shè)計模式:一個被稱作被觀察者的對象,維護(hù)一組被稱為觀察者的對象,這些對象依賴于被觀察者,被觀察者自動將自身的狀態(tài)的任何變化通知給它們。

        當(dāng)一個被觀察者需要將一些變化通知給觀察者的時候,它將采用廣播的方式,這條廣播可能包含特定于這條通知的一些數(shù)據(jù)。

        當(dāng)特定的觀察者不再需要接受來自于它所注冊的被觀察者的通知的時候,被觀察者可以將其從所維護(hù)的組中刪除。在這里提及一下設(shè)計模式現(xiàn)有的定義很有必要。這個定義是與所使用的語言無關(guān)的。

        通過這個定義,最終我們可以更深層次地了解到設(shè)計模式如何使用以及其優(yōu)勢。在四人幫的《設(shè)計模式:可重用的面向?qū)ο筌浖脑亍愤@本書中,是這樣定義觀察者模式的:

        一個或者更多的觀察者對一個被觀察者的狀態(tài)感興趣,將自身的這種興趣通過附著自身的方式注冊在被觀察者身上。當(dāng)被觀察者發(fā)生變化,而這種便可也是觀察者所關(guān)心的,就會產(chǎn)生一個通知,這個通知將會被送出去,最后將會調(diào)用每個觀察者的更新方法。當(dāng)觀察者不在對被觀察者的狀態(tài)感興趣的時候,它們只需要簡單的將自身剝離即可。

        我們現(xiàn)在可以通過實現(xiàn)一個觀察者模式來進(jìn)一步擴(kuò)展我們剛才所學(xué)到的東西。這個實現(xiàn)包含一下組件:

        • 被觀察者:維護(hù)一組觀察者, 提供用于增加和移除觀察者的方法。

        • 觀察者:提供一個更新接口,用于當(dāng)被觀察者狀態(tài)變化時,得到通知。

        • 具體的被觀察者:狀態(tài)變化時廣播通知給觀察者,保持具體的觀察者的信息。

        • 具體的觀察者:保持一個指向具體被觀察者的引用,實現(xiàn)一個更新接口,用于觀察,以便保證自身狀態(tài)總是和被觀察者狀態(tài)一致的。

        首先,讓我們對被觀察者可能有的一組依賴其的觀察者進(jìn)行建模:

        function ObserverList(){  this.observerList = [];}ObserverList.prototype.Add = function( obj ){  return this.observerList.push( obj );};ObserverList.prototype.Empty = function(){  this.observerList = [];};ObserverList.prototype.Count = function(){  return this.observerList.length;};ObserverList.prototype.Get = function( index ){  if( index > -1 && index < this.observerList.length ){    return this.observerList[ index ];  }};ObserverList.prototype.Insert = function( obj, index ){  let pointer = -1;  if( index === 0 ){    this.observerList.unshift( obj );    pointer = index;  }else if( index === this.observerList.length ){    this.observerList.push( obj );    pointer = index;  }  return pointer;};ObserverList.prototype.IndexOf = function( obj, startIndex ){  let i = startIndex, pointer = -1;  while( i < this.observerList.length ){    if( this.observerList[i] === obj ){      pointer = i;    }    i++;  }  return pointer;};ObserverList.prototype.RemoveAt = function( index ){  if( index === 0 ){    this.observerList.shift();  }else if( index === this.observerList.length -1 ){    this.observerList.pop();  }};// Extend an object with an extensionfunction extend( extension, obj ){  for ( let key in extension ){    obj[key] = extension[key];  }}

        接著,我們對被觀察者以及其增加,刪除,通知在觀察者列表中的觀察者的能力進(jìn)行建模:

        function Subject(){  this.observers = new ObserverList();}Subject.prototype.AddObserver = function( observer ){  this.observers.Add( observer );}; Subject.prototype.RemoveObserver = function( observer ){  this.observers.RemoveAt( this.observers.IndexOf( observer, 0 ) );}; Subject.prototype.Notify = function( context ){  let observerCount = this.observers.Count();  for(let i=0; i < observerCount; i++){    this.observers.Get(i).Update( context );  }};

        我們接著定義建立新的觀察者的一個框架。這里的update 函數(shù)之后會被具體的行為覆蓋。

        // The Observerfunction Observer(){  this.Update = function(){    // ...  };}

        在我們的樣例應(yīng)用里面,我們使用上面的觀察者組件,現(xiàn)在我們定義:

        • 一個按鈕,這個按鈕用于增加新的充當(dāng)觀察者的選擇框到頁面上

        • 一個控制用的選擇框 , 充當(dāng)一個被觀察者,通知其它選擇框是否應(yīng)該被選中

        • 一個容器,用于放置新的選擇框

        我們接著定義具體被觀察者和具體觀察者,用于給頁面增加新的觀察者,以及實現(xiàn)更新接口。通過查看下面的內(nèi)聯(lián)的注釋,搞清楚在我們樣例中的這些組件是如何工作的。

        html

        Javascript

        // 我們DOM 元素的引用let controlCheckbox = document.getElementById("mainCheckbox"),  addBtn = document.getElementById( "addNewObserver" ),  container = document.getElementById( "observersContainer" );// 具體的被觀察者//Subject 類擴(kuò)展controlCheckbox 類extend( new Subject(), controlCheckbox );//點(diǎn)擊checkbox 將會觸發(fā)對觀察者的通知controlCheckbox["onclick"] = new Function("controlCheckbox.Notify(controlCheckbox.checked)");addBtn["onclick"] = AddNewObserver;// 具體的觀察者function AddNewObserver(){  //建立一個新的用于增加的checkbox  let check  = document.createElement( "input" );  check.type = "checkbox";  // 使用Observer 類擴(kuò)展checkbox  extend( new Observer(), check );  // 使用定制的Update函數(shù)重載  check.Update = function( value ){    this.checked = value;  };  // 增加新的觀察者到我們主要的被觀察者的觀察者列表中  controlCheckbox.AddObserver( check );  // 將元素添加到容器的最后  container.appendChild( check );}

        在這個例子里面,我們看到了如何實現(xiàn)和配置觀察者模式,了解了被觀察者,觀察者,具體被觀察者,具體觀察者的概念。

        觀察者模式和發(fā)布/訂閱模式的不同

        觀察者模式確實很有用,但是在javascript時間里面,通常我們使用一種叫做發(fā)布/訂閱模式的變體來實現(xiàn)觀察者模式。這兩種模式很相似,但是也有一些值得注意的不同。

        觀察者模式要求想要接受相關(guān)通知的觀察者必須到發(fā)起這個事件的被觀察者上注冊這個事件。

        發(fā)布/訂閱模式使用一個主題/事件頻道,這個頻道處于想要獲取通知的訂閱者和發(fā)起事件的發(fā)布者之間。

        這個事件系統(tǒng)允許代碼定義應(yīng)用相關(guān)的事件,這個事件可以傳遞特殊的參數(shù),參數(shù)中包含有訂閱者所需要的值。這種想法是為了避免訂閱者和發(fā)布者之間的依賴性。

        這種和觀察者模式之間的不同,使訂閱者可以實現(xiàn)一個合適的事件處理函數(shù),用于注冊和接受由發(fā)布者廣播的相關(guān)通知。

        這里給出一個關(guān)于如何使用發(fā)布者/訂閱者模式的例子,這個例子中完整地實現(xiàn)了功能強(qiáng)大的publish(), subscribe() 和 unsubscribe()。

        // 一個非常簡單的郵件處理器// 接受的消息的計數(shù)器let mailCounter = 0;// 初始化一個訂閱者,這個訂閱者監(jiān)聽名叫"inbox/newMessage" 的頻道// 渲染新消息的粗略信息let subscriber1 = subscribe( "inbox/newMessage", function( topic, data ) {  // 日志記錄主題,用于調(diào)試  console.log( "A new message was received: ", topic );  // 使用來自于被觀察者的數(shù)據(jù),用于給用戶展示一個消息的粗略信息  $( ".messageSender" ).html( data.sender );  $( ".messagePreview" ).html( data.body );});// 這是另外一個訂閱者,使用相同的數(shù)據(jù)執(zhí)行不同的任務(wù)// 更細(xì)計數(shù)器,顯示當(dāng)前來自于發(fā)布者的新信息的數(shù)量let subscriber2 = subscribe( "inbox/newMessage", function( topic, data ) {  $('.newMessageCounter').html( mailCounter++ );});publish( "inbox/newMessage", [{  sender:"[email protected]",  body: "Hey there! How are you doing today?"}]);// 在之后,我們可以讓我們的訂閱者通過下面的方式取消訂閱來自于新主題的通知// unsubscribe( subscriber1,  );// unsubscribe( subscriber2 );

        這個例子的更廣的意義是對松耦合的原則的一種推崇。不是一個對象直接調(diào)用另外一個對象的方法,而是通過訂閱另外一個對象的一個特定的任務(wù)或者活動,從而在這個任務(wù)或者活動出現(xiàn)的時候的得到通知。

        優(yōu)點(diǎn)

        觀察者和發(fā)布/訂閱模式鼓勵人們認(rèn)真考慮應(yīng)用不同部分之間的關(guān)系,同時幫助我們找出這樣的層,該層中包含有直接的關(guān)系,這些關(guān)系可以通過一些列的觀察者和被觀察者來替換掉。

        這中方式可以有效地將一個應(yīng)用程序切割成小塊,這些小塊耦合度低,從而改善代碼的管理,以及用于潛在的代碼復(fù)用。

        使用觀察者模式更深層次的動機(jī)是,當(dāng)我們需要維護(hù)相關(guān)對象的一致性的時候,我們可以避免對象之間的緊密耦合。例如,一個對象可以通知另外一個對象,而不需要知道這個對象的信息。

        兩種模式下,觀察者和被觀察者之間都可以存在動態(tài)關(guān)系。這提供很好的靈活性,而當(dāng)我們的應(yīng)用中不同的部分之間緊密耦合的時候,是很難實現(xiàn)這種靈活性的。

        盡管這些模式并不是萬能的靈丹妙藥,這些模式仍然是作為最好的設(shè)計松耦合系統(tǒng)的工具之一,因此在任何的JavaScript 開發(fā)者的工具箱里面,都應(yīng)該有這樣一個重要的工具。

        缺點(diǎn)

        事實上,這些模式的一些問題實際上正是來自于它們所帶來的一些好處。在發(fā)布/訂閱模式中,將發(fā)布者共訂閱者上解耦,將會在一些情況下,導(dǎo)致很難確保我們應(yīng)用中的特定部分按照我們預(yù)期的那樣正常工作。

        例如,發(fā)布者可以假設(shè)有一個或者多個訂閱者正在監(jiān)聽它們。比如我們基于這樣的假設(shè),在某些應(yīng)用處理過程中來記錄或者輸出錯誤日志。如果訂閱者執(zhí)行日志功能崩潰了(或者因為某些原因不能正常工作),因為系統(tǒng)本身的解耦本質(zhì),發(fā)布者沒有辦法感知到這些事情。

        另外一個這種模式的缺點(diǎn)是,訂閱者對彼此之間存在沒有感知,對切換發(fā)布者的代價無從得知。因為訂閱者和發(fā)布者之間的動態(tài)關(guān)系,更新依賴也很能去追蹤。

        讓我們看一下最小的一個版本的發(fā)布/訂閱模式實現(xiàn)。這個實現(xiàn)展示了發(fā)布,訂閱的核心概念,以及如何取消訂閱。

        let pubsub = {};(function(q) {    let topics = {},        subUid = -1;    q.publish = function( topic, args ) {        if ( !topics[topic] ) {            return false;        }        let subscribers = topics[topic],            len = subscribers ? subscribers.length : 0;        while (len--) {            subscribers[len].func( topic, args );        }        return this;    };    q.subscribe = function( topic, func ) {        if (!topics[topic]) {            topics[topic] = [];        }        let token = ( ++subUid ).toString();        topics[topic].push({            token: token,            func: func        });        return token;    };    q.unsubscribe = function( token ) {        for ( let m in topics ) {            if ( topics[m] ) {                for ( let i = 0, j = topics[m].length; i < j; i++ ) {                    if ( topics[m][i].token === token) {                        topics[m].splice( i, 1 );                        return token;                    }                }            }        }        return this;    };}( pubsub ));

        我們現(xiàn)在可以使用發(fā)布實例和訂閱感興趣的事件,例如:

        let messageLogger = function ( topics, data ) {    console.log( "Logging: " + topics + ": " + data );};let subscription = pubsub.subscribe( "inbox/newMessage", messageLogger );pubsub.publish( "inbox/newMessage", "hello world!" );// orpubsub.publish( "inbox/newMessage", ["test", "a", "b", "c"] );// orpubsub.publish( "inbox/newMessage", {  sender: "[email protected]",  body: "Hey again!"});// We cab also unsubscribe if we no longer wish for our subscribers// to be notified// pubsub.unsubscribe( subscription );pubsub.publish( "inbox/newMessage", "Hello! are you still there?" );

        觀察者模式在應(yīng)用設(shè)計中,解耦一系列不同的場景上非常有用,如果你沒有用過它,我推薦你嘗試一下今天提到的之前寫到的某個實現(xiàn)。這個模式是一個易于學(xué)習(xí)的模式,同時也是一個威力巨大的模式。

        5、中介者模式

        如果系統(tǒng)組件之間存在大量的直接關(guān)系,就可能是時候,使用一個中心的控制點(diǎn),來讓不同的組件通過它來通信。中介者通過將組件之間顯式的直接的引用替換成通過中心點(diǎn)來交互的方式,來做到松耦合。這樣可以幫助我們解耦,和改善組件的重用性。

        在現(xiàn)實世界中,類似的系統(tǒng)就是,飛行控制系統(tǒng)。一個航站塔(中介者)處理哪個飛機(jī)可以起飛,哪個可以著陸,因為所有的通信(監(jiān)聽的通知或者廣播的通知)都是飛機(jī)和控制塔之間進(jìn)行的,而不是飛機(jī)和飛機(jī)之間進(jìn)行的。一個中央集權(quán)的控制中心是這個系統(tǒng)成功的關(guān)鍵,也正是中介者在軟件設(shè)計領(lǐng)域中所扮演的角色。

        5.1基礎(chǔ)的實現(xiàn)

        中間人模式的一種簡單的實現(xiàn)可以在下面找到,publish()和subscribe()方法都被暴露出來使用:

        let mediator = (function(){    let topics = {};    let subscribe = function( topic, fn ){        if ( !topics[topic] ){          topics[topic] = [];        }        topics[topic].push( { context: this, callback: fn } );        return this;    };    let publish = function( topic ){        let args;        if ( !topics[topic] ){          return false;        }        args = Array.prototype.slice.call( arguments, 1 );        for ( let i = 0, l = topics[topic].length; i < l; i++ ) {            let subscription = topics[topic][i];            subscription.callback.apply( subscription.context, args );        }        return this;    };    return {        publish: publish,        subscribe: subscribe,        installTo: function( obj ){            obj.subscribe = subscribe;            obj.publish = publish;        }    };}());

        優(yōu)點(diǎn) & 缺點(diǎn)

        中間人模式最大的好處就是,它節(jié)約了對象或者組件之間的通信信道,這些對象或者組件存在于從多對多到多對一的系統(tǒng)之中。由于解耦合水平的因素,添加新的發(fā)布或者訂閱者是相對容易的。

        也許使用這個模式最大的缺點(diǎn)是它可以引入一個單點(diǎn)故障。在模塊之間放置一個中間人也可能會造成性能損失,因為它們經(jīng)常是間接地的進(jìn)行通信的。由于松耦合的特性,僅僅盯著廣播很難去確認(rèn)系統(tǒng)是如何做出反應(yīng)的。

        這就是說,提醒我們自己解耦合的系統(tǒng)擁有許多其它的好處,是很有用的——如果我們的模塊互相之間直接的進(jìn)行通信,對于模塊的改變(例如:另一個模塊拋出了異常)可以很容易的對我們系統(tǒng)的其它部分產(chǎn)生多米諾連鎖效應(yīng)。這個問題在解耦合的系統(tǒng)中很少需要被考慮到。

        在一天結(jié)束的時候,緊耦合會導(dǎo)致各種頭痛,這僅僅只是另外一種可選的解決方案,但是如果得到正確實現(xiàn)的話也能夠工作得很好。

        6、原型模式

        原型模式是指通過克隆的方式基于一個現(xiàn)有對象的模板創(chuàng)建對象的模式。

        我們能夠?qū)⒃湍J秸J(rèn)作是基于原型的繼承中,我們創(chuàng)建作為其它對象原型的對象.原型對象自身被當(dāng)做構(gòu)造器創(chuàng)建的每一個對象的藍(lán)本高效的使用著.如果構(gòu)造器函數(shù)使用的原型包含例如叫做name的屬性,那么每一個通過同一個構(gòu)造器創(chuàng)建的對象都將擁有這個相同的屬性。

        我們可以在下面的示例中看到對這個的展示:

        let myCar = {  name: "Ford Escort",  drive: function () {    console.log( "Weeee. I'm driving!" );  },  panic: function () {    console.log( "Wait. How do you stop this thing?" );  }};let yourCar = Object.create( myCar );console.log( yourCar.name );// Ford Escort

        Object.create也允許我們簡單的繼承先進(jìn)的概念,比如對象能夠直接繼承自其它對象,這種不同的繼承.我們早先也看到Object.create允許我們使用 供應(yīng)的第二個參數(shù)來初始化對象屬性。例如:

        let vehicle = {  getModel: function () {    console.log( "The model of this vehicle is.." + this.model );  }};let car = Object.create(vehicle, {  "id": {    value: "1",    // writable:false, configurable:false by default    enumerable: true  },  "model": {    value: "Ford",    enumerable: true  }});

        這里的屬性可以被Object.create的第二個參數(shù)來初始化,使用一種類似于Object.defineProperties和Object.defineProperties方法所使用語法的對象字面值。

        在枚舉對象的屬性,和在一個hasOwnProperty()檢查中封裝循環(huán)的內(nèi)容時,原型關(guān)系會造成麻煩,這一事實是值得我們關(guān)注的。

        如果我們希望在不直接使用Object.create的前提下實現(xiàn)原型模式,我們可以像下面這樣,按照上面的示例,模擬這一模式:

        let vehiclePrototype = {  init: function ( carModel ) {    this.model = carModel;  },  getModel: function () {    console.log( "The model of this vehicle is.." + this.model);  }};function vehicle( model ) {  function F() {};  F.prototype = vehiclePrototype;  let f = new F();  f.init( model );  return f;}let car = vehicle( "Ford Escort" );car.getModel();

        注意:這種可選的方式不允許用戶使用相同的方式定義只讀的屬性(因為如果不小心的話vehicle原型可能會被改變)。

        原型模式的最后一種可選實現(xiàn)可以像下面這樣:

        let beget = (function () {    function F() {}    return function ( proto ) {        F.prototype = proto;        return new F();    };})();

        7、命令模式

        命名模式的目標(biāo)是將方法的調(diào)用,請求或者操作封裝到一個單獨(dú)的對象中,給我們酌情執(zhí)行同時參數(shù)化和傳遞方法調(diào)用的能力.另外,它使得我們能將對象從實現(xiàn)了行為的對象對這些行為的調(diào)用進(jìn)行解耦,為我們帶來了換出具體的對象這一更深程度的整體靈活性。

        具體類是對基于類的編程語言的最好解釋,并且同抽象類的理念聯(lián)系緊密。抽象類定義了一個接口,但并不需要提供對它的所有成員函數(shù)的實現(xiàn)。它扮演著驅(qū)動其它類的基類角色.被驅(qū)動類實現(xiàn)了缺失的函數(shù)而被稱為具體類.。命令模式背后的一般理念是為我們提供了從任何執(zhí)行中的命令中分離出發(fā)出命令的責(zé)任,取而代之將這一責(zé)任委托給其它的對象。

        實現(xiàn)明智簡單的命令對象,將一個行為和對象對調(diào)用這個行為的需求都綁定到了一起.它們始終都包含一個執(zhí)行操作(比如run()或者execute()).所有帶有相同接口的命令對象能夠被簡單地根據(jù)需要調(diào)換,這被認(rèn)為是命令模式的更大的好處之一。

        為了展示命令模式,我們創(chuàng)建一個簡單的汽車購買服務(wù):

        (function(){  let CarManager = {      requestInfo: function( model, id ){        return "The information for " + model + " with ID " + id + " is foobar";      },      buyVehicle: function( model, id ){        return "You have successfully purchased Item " + id + ", a " + model;      },      arrangeViewing: function( model, id ){        return "You have successfully booked a viewing of " + model + " ( " + id + " ) ";      }    };})();

        看一看上面的這段代碼,它也許是通過直接訪問對象來瑣碎的調(diào)用我們CarManager的方法。在技術(shù)上我們也許都會都會對這個沒有任何失誤達(dá)成諒解.它是完全有效的Javascript然而也會有情況不利的情況。

        例如,想象如果CarManager的核心API會發(fā)生改變的這種情況.這可能需要所有直接訪問這些方法的對象也跟著被修改.這可以被看成是一種耦合,明顯違背了OOP方法學(xué)盡量實現(xiàn)松耦合的理念.取而代之,我們可以通過更深入的抽象這些API來解決這個問題。

        現(xiàn)在讓我們來擴(kuò)展我們的CarManager,以便我們這個命令模式的應(yīng)用程序得到接下來的這種效果:接受任何可以在CarManager對象上面執(zhí)行的方法,傳送任何可以被使用到的數(shù)據(jù),如Car模型和ID。

        這里是我們希望能夠?qū)崿F(xiàn)的樣子:

        CarManager.execute( "buyVehicle", "Ford Escort", "453543" );

        按照這種結(jié)構(gòu),我們現(xiàn)在應(yīng)該像下面這樣,添加一個對于"CarManager.execute()"方法的定義:

        CarManager.execute = function ( name ) {    return CarManager[name] && CarManager[name].apply( CarManager, [].slice.call(arguments, 1) );};

        最終我們的調(diào)用如下所示:

        CarManager.execute( "arrangeViewing", "Ferrari", "14523" );CarManager.execute( "requestInfo", "Ford Mondeo", "54323" );CarManager.execute( "requestInfo", "Ford Escort", "34232" );CarManager.execute( "buyVehicle", "Ford Escort", "34232" );

        8、外觀模式

        當(dāng)我們提出一個門面,我們要向這個世界展現(xiàn)的是一個外觀,這一外觀可能藏匿著一種非常與眾不同的真實。這就是我們即將要回顧的模式背后的靈感——門面模式。

        這一模式提供了面向一種更大型的代碼體提供了一個的更高級別的舒適的接口,隱藏了其真正的潛在復(fù)雜性。

        把這一模式想象成要是呈現(xiàn)給開發(fā)者簡化的API,一些總是會提升使用性能的東西。

        為了在我們所學(xué)的基礎(chǔ)上進(jìn)行構(gòu)建,門面模式同時需要簡化一個類的接口,和把類同使用它的代碼解耦。這給予了我們使用一種方式直接同子系統(tǒng)交互的能力,這一方式有時候會比直接訪問子系統(tǒng)更加不容易出錯。

        門面的優(yōu)勢包括易用,還有常常實現(xiàn)起這個模式來只是一小段路,不費(fèi)力。

        讓我們通過實踐來看看這個模式。這是一個沒有經(jīng)過優(yōu)化的代碼示例,但是這里我們使用了一個門面來簡化跨瀏覽器事件監(jiān)聽的接口。我們創(chuàng)建了一個公共的方法來實現(xiàn),此方法能夠被用在檢查特性的存在的代碼中,以便這段代碼能夠提供一種安全和跨瀏覽器兼容方案。

        let addMyEvent = function( el,ev,fn ){   if( el.addEventListener ){       el.addEventListener( ev,fn, false );   }else if(el.attachEvent){       el.attachEvent( "on" + ev, fn );   }else{       el["on" + ev] = fn;   }};

        門面不僅僅只被用在它們自己身上,它們也能夠被用來同其它的模式諸如模塊模式進(jìn)行集成。如我們在下面所看到的,我們模塊模式的實體包含許多被定義為私有的方法。門面則被用來提供訪問這些方法的更加簡單的API:

        let module = (function() {    let _private = {        i:5,        get : function() {            console.log( "current value:" + this.i);        },        set : function( val ) {            this.i = val;        },        run : function() {            console.log( "running" );        },        jump: function(){            console.log( "jumping" );        }    };    return {        facade : function( args ) {            _private.set(args.val);            _private.get();            if ( args.run ) {                _private.run();            }        }    };}());module.facade( {run: true, val:10} );// "current value: 10" and "running"

        在這個示例中,調(diào)用module.facade()將會觸發(fā)一堆模塊中的私有方法。但再一次,用戶并不需要關(guān)心這些。我們已經(jīng)使得對用戶而言不需要擔(dān)心實現(xiàn)級別的細(xì)節(jié)就能消受一種特性。

        9、工廠模式

        工廠模式是另外一種關(guān)注對象創(chuàng)建概念的創(chuàng)建模式。它的領(lǐng)域中同其它模式的不同之處在于它并沒有明確要求我們使用一個構(gòu)造器。

        取而代之,一個工廠能提供一個創(chuàng)建對象的公共接口,我們可以在其中指定我們希望被創(chuàng)建的工廠對象的類型。

        下面我們通過使用構(gòu)造器模式邏輯來定義汽車。這個例子展示了Vehicle 工廠可以使用工廠模式來實現(xiàn)。

        function Car( options ) {  this.doors = options.doors || 4;  this.state = options.state || "brand new";  this.color = options.color || "silver";
        }function Truck( options){ this.state = options.state || "used"; this.wheelSize = options.wheelSize || "large"; this.color = options.color || "blue";}function VehicleFactory() {}VehicleFactory.prototype.vehicleClass = Car;VehicleFactory.prototype.createVehicle = function ( options ) { if( options.vehicleType === "car" ){ this.vehicleClass = Car; }else{ this.vehicleClass = Truck; } return new this.vehicleClass( options );
        };let carFactory = new VehicleFactory();let car = carFactory.createVehicle( { vehicleType: "car", color: "yellow", doors: 6 } );console.log( car );

        何時使用工廠模式

        當(dāng)被應(yīng)用到下面的場景中時,工廠模式特別有用:

        • 當(dāng)我們的對象或者組件設(shè)置涉及到高程度級別的復(fù)雜度時。

        • 當(dāng)我們需要根據(jù)我們所在的環(huán)境方便的生成不同對象的實體時。

        • 當(dāng)我們在許多共享同一個屬性的許多小型對象或組件上工作時。

        • 當(dāng)帶有其它僅僅需要滿足一種API約定(又名鴨式類型)的對象的組合對象工作時.這對于解耦來說是有用的。

        何時不要去使用工廠模式

        當(dāng)被應(yīng)用到錯誤的問題類型上時,這一模式會給應(yīng)用程序引入大量不必要的復(fù)雜性.除非為創(chuàng)建對象提供一個接口是我們編寫的庫或者框架的一個設(shè)計上目標(biāo),否則我會建議使用明確的構(gòu)造器,以避免不必要的開銷。

        由于對象的創(chuàng)建過程被高效的抽象在一個接口后面的事實,這也會給依賴于這個過程可能會有多復(fù)雜的單元測試帶來問題。

        抽象工廠

        了解抽象工廠模式也是非常實用的,它的目標(biāo)是以一個通用的目標(biāo)將一組獨(dú)立的工廠進(jìn)行封裝.它將一堆對象的實現(xiàn)細(xì)節(jié)從它們的一般用例中分離。

        抽象工廠應(yīng)該被用在一種必須從其創(chuàng)建或生成對象的方式處獨(dú)立,或者需要同多種類型的對象一起工作,這樣的系統(tǒng)中。

        簡單且容易理解的例子就是一個發(fā)動機(jī)工廠,它定義了獲取或者注冊發(fā)動機(jī)類型的方式。抽象工廠會被命名為AbstractVehicleFactory。抽象工廠將允許像"car"或者"truck"的發(fā)動機(jī)類型的定義,并且構(gòu)造工廠將僅實現(xiàn)滿足發(fā)動機(jī)合同的類.(例如:Vehicle.prototype.driven和Vehicle.prototype.breakDown)。

        let AbstractVehicleFactory = (function () {    let types = {};    return {        getVehicle: function ( type, customizations ) {            var Vehicle = types[type];            return (Vehicle ? new Vehicle(customizations) : null);        },        registerVehicle: function ( type, Vehicle ) {            let proto = Vehicle.prototype;            // only register classes that fulfill the vehicle contract            if ( proto.drive && proto.breakDown ) {                types[type] = Vehicle;            }            return AbstractVehicleFactory;        }    };})();
        AbstractVehicleFactory.registerVehicle( "car", Car );AbstractVehicleFactory.registerVehicle( "truck", Truck );
        let car = AbstractVehicleFactory.getVehicle( "car" , { color: "lime green", state: "like new" } );
        let truck = AbstractVehicleFactory.getVehicle( "truck" , { wheelSize: "medium", color: "neon yellow" } );

        10、Mixin 模式

        mixin模式指一些提供能夠被一個或者一組子類簡單繼承功能的類,意在重用其功能。

        子類劃分

        子類劃分是一個參考了為一個新對象繼承來自一個基類或者超類對象的屬性的術(shù)語.在傳統(tǒng)的面向?qū)ο缶幊讨?類B能夠從另外一個類A處擴(kuò)展。這里我們將A看做是超類,而將B看做是A的子類。如此,所有B的實體都從A處繼承了其A的方法,然而B仍然能夠定義它自己的方法,包括那些重載的原本在A中的定義的方法。

        B是否應(yīng)該調(diào)用已經(jīng)被重載的A中的方法,我們將這個引述為方法鏈.B是否應(yīng)該調(diào)用A(超類)的構(gòu)造器,我們將這稱為構(gòu)造器鏈。

        為了演示子類劃分,首先我們需要一個能夠創(chuàng)建自身新實體的基對象。

        let Person =  function( firstName , lastName ){  this.firstName = firstName;  this.lastName =  lastName;  this.gender = "male";};

        接下來,我們將制定一個新的類(對象),它是一個現(xiàn)有的Person對象的子類.讓我們想象我們想要加入一個不同屬性用來分辨一個Person和一個繼承了Person"超類"屬性的Superhero.由于超級英雄分享了一般人類許多共有的特征(例如:name,gender),因此這應(yīng)該很有希望充分展示出子類劃分是如何工作的。

        let clark = new Person( "Clark" , "Kent" );let Superhero = function( firstName, lastName , powers ){    Person.call( this, firstName, lastName );    this.powers = powers;};SuperHero.prototype = Object.create( Person.prototype );let superman = new Superhero( "Clark" ,"Kent" , ["flight","heat-vision"] );console.log( superman );

        Superhero構(gòu)造器創(chuàng)建了一個自Peroson下降的對象。這種類型的對象擁有鏈中位于它之上的對象的屬性,而且如果我們在Person對象中設(shè)置了默認(rèn)的值,Superhero能夠使用特定于它的對象的值覆蓋任何繼承的值。

        Mixin(織入目標(biāo)類)

        在Javascript中,我們會將從Mixin繼承看作是通過擴(kuò)展收集功能的一種途徑.我們定義的每一個新的對象都有一個原型,從其中它可以繼承更多的屬性.原型可以從其他對象繼承而來,但是更重要的是,能夠為任意數(shù)量的對象定義屬性.我們可以利用這一事實來促進(jìn)功能重用。

        Mix允許對象以最小量的復(fù)雜性從它們那里借用(或者說繼承)功能.作為一種利用Javascript對象原型工作得很好的模式,它為我們提供了從不止一個Mix處分享功能的相當(dāng)靈活,但比多繼承有效得多得多的方式。

        它們可以被看做是其屬性和方法可以很容易的在其它大量對象原型共享的對象.想象一下我們定義了一個在一個標(biāo)準(zhǔn)對象字面量中含有實用功能的Mixin,如下所示:

        let myMixins = {
        moveUp: function(){ console.log( "move up" ); },
        moveDown: function(){ console.log( "move down" ); },
        stop: function(){ console.log( "stop! in the name of love!" ); }
        };

        然后我們可以方便的擴(kuò)展現(xiàn)有構(gòu)造器功能的原型,使其包含這種使用一個 如下面的score.js_.extends()方法輔助器的行為:

        function carAnimator(){  this.moveLeft = function(){    console.log( "move left" );  };}function personAnimator(){  this.moveRandomly = function(){ /*..*/ };}_.extend( carAnimator.prototype, myMixins );_.extend( personAnimator.prototype, myMixins );let myAnimator = new carAnimator();myAnimator.moveLeft();myAnimator.moveDown();myAnimator.stop();

        如我們所見,這允許我們將通用的行為輕易的"混"入相當(dāng)普通對象構(gòu)造器中。

        在接下來的示例中,我們有兩個構(gòu)造器:一個Car和一個Mixin.我們將要做的是靜Car參數(shù)化(另外一種說法是擴(kuò)展),以便它能夠繼承Mixin中的特定方法,名叫driveForwar()和driveBackward().這一次我們不會使用Underscore.js。

        取而代之,這個示例將演示如何將一個構(gòu)造器參數(shù)化,以便在無需重復(fù)每一個構(gòu)造器函數(shù)過程的前提下包含其功能。

        let Car = function ( settings ) {    this.model = settings.model || "no model provided";    this.color = settings.color || "no colour provided";};// Mixinlet Mixin = function () {};Mixin.prototype = {    driveForward: function () {        console.log( "drive forward" );    },    driveBackward: function () {        console.log( "drive backward" );    },    driveSideways: function () {        console.log( "drive sideways" );    }};function augment( receivingClass, givingClass ) {    if ( arguments[2] ) {        for ( var i = 2, len = arguments.length; i < len; i++ ) {            receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];        }    }else {        for ( let methodName in givingClass.prototype ) {            if ( !Object.hasOwnProperty(receivingClass.prototype, methodName) ) {                receivingClass.prototype[methodName] = givingClass.prototype[methodName];            }        }    }}augment( Car, Mixin, "driveForward", "driveBackward" );let myCar = new Car({    model: "Ford Escort",    color: "blue"});myCar.driveForward();myCar.driveBackward();
        augment( Car, Mixin );let mySportsCar = new Car({ model: "Porsche", color: "red"});mySportsCar.driveSideways();

        優(yōu)點(diǎn) & 缺點(diǎn)

        Mixin支持在一個系統(tǒng)中降解功能的重復(fù)性,增加功能的重用性.在一些應(yīng)用程序也許需要在所有的對象實體共享行為的地方,我們能夠通過在一個Mixin中維護(hù)這個共享的功能,來很容易的避免任何重復(fù),而因此專注于只實現(xiàn)我們系統(tǒng)中真正彼此不同的功能。

        也就是說,對Mixin的副作用是值得商榷的.一些開發(fā)者感覺將功能注入到對象的原型中是一個壞點(diǎn)子,因為它會同時導(dǎo)致原型污染和一定程度上的對我們原有功能的不確定性.在大型的系統(tǒng)中,很可能是有這種情況的。

        但是,強(qiáng)大的文檔對最大限度的減少對待功能中的混入源的迷惑是有幫助的,而且對于每一種模式而言,如果在實現(xiàn)過程中小心行事,我們應(yīng)該是沒多大問題的。

        11、裝飾器模式

        裝飾器是旨在提升重用性能的一種結(jié)構(gòu)性設(shè)計模式。同Mixin類似,它可以被看作是應(yīng)用子類劃分的另外一種有價值的可選方案。

        典型的裝飾器提供了向一個系統(tǒng)中現(xiàn)有的類動態(tài)添加行為的能力。其創(chuàng)意是裝飾本身并不關(guān)心類的基礎(chǔ)功能,而只是將它自身拷貝到超類之中。

        裝飾器模式并不去深入依賴于對象是如何創(chuàng)建的,而是專注于擴(kuò)展它們的功能這一問題上。不同于只依賴于原型繼承,我們在一個簡單的基礎(chǔ)對象上面逐步添加能夠提供附加功能的裝飾對象。它的想法是,不同于子類劃分,我們向一個基礎(chǔ)對象添加(裝飾)屬性或者方法,因此它會是更加輕巧的。

        向Javascript中的對象添加新的屬性是一個非常直接了當(dāng)?shù)倪^程,因此將這一特定牢記于心,一個非常簡單的裝飾器可以實現(xiàn)如下:

        示例1:帶有新功能的裝飾構(gòu)造器

        function vehicle( vehicleType ){    this.vehicleType = vehicleType || "car";    this.model = "default";    this.license = "00000-000";}let testInstance = new vehicle( "car" );console.log( testInstance );// vehicle: car, model:default, license: 00000-000
        let truck = new vehicle( "truck" );truck.setModel = function( modelName ){ this.model = modelName;};truck.setColor = function( color ){ this.color = color;};truck.setModel( "CAT" );truck.setColor( "blue" );console.log( truck );// vehicle:truck, model:CAT, color: blue
        let secondInstance = new vehicle( "car" );console.log( secondInstance );// vehicle: car, model:default, license: 00000-000

        示例2:帶有多個裝飾器的裝飾對象

        function MacBook() {  this.cost = function () { return 997; };  this.screenSize = function () { return 11.6; };}function Memory( macbook ) {  let v = macbook.cost();  macbook.cost = function() {    return v + 75;  };}function Engraving( macbook ){  let v = macbook.cost();  macbook.cost = function(){    return  v + 200;  };}function Insurance( macbook ){  let v = macbook.cost();  macbook.cost = function(){     return  v + 250;  };}
        let mb = new MacBook();Memory( mb );Engraving( mb );Insurance( mb );console.log( mb.cost() );// 1522console.log( mb.screenSize() );// 11.6

        在上面的示例中,我們的裝飾器重載了超類對象MacBook()的 object.cost()函數(shù),使其返回的Macbook的當(dāng)前價格加上了被定制后升級的價格。

        這被看做是對原來的Macbook對象構(gòu)造器方法的裝飾,它并沒有將其重寫(例如,screenSize()),我們所定義的Macbook的其它屬性也保持不變,完好無缺。

        優(yōu)點(diǎn) & 缺點(diǎn)

        因為它可以被透明的使用,并且也相當(dāng)?shù)撵`活,因此開發(fā)者都挺樂意去使用這個模式——如我們所見,對象可以用新的行為封裝或者“裝飾”起來,而后繼續(xù)使用,并不用去擔(dān)心基礎(chǔ)的對象被改變。在一個更加廣泛的范圍內(nèi),這一模式也避免了我們?nèi)ヒ蕾嚧罅孔宇悂韺崿F(xiàn)同樣的效果。

        然而在實現(xiàn)這個模式時,也存在我們應(yīng)該意識到的缺點(diǎn)。如果窮于管理,它也會由于引入了許多微小但是相似的對象到我們的命名空間中,從而顯著的使得我們的應(yīng)用程序架構(gòu)變得復(fù)雜起來。這里所擔(dān)憂的是,除了漸漸變得難于管理,其他不能熟練使用這個模式的開發(fā)者也可能會有一段要掌握它被使用的理由的艱難時期。

        足夠的注釋或者對模式的研究,對此應(yīng)該有助益,而只要我們對在我們的應(yīng)程序中的多大范圍內(nèi)使用這一模式有所掌控的話,我們就能讓兩方面都得到改善。

        12、亨元模式

        享元模式是一個優(yōu)化重復(fù)、緩慢和低效數(shù)據(jù)共享代碼的經(jīng)典結(jié)構(gòu)化解決方案。它的目標(biāo)是以相關(guān)對象盡可能多的共享數(shù)據(jù),來減少應(yīng)用程序中內(nèi)存的使用(例如:應(yīng)用程序的配置、狀態(tài)等)。

        此模式最先由Paul Calder 和 Mark Linton在1990提出,并用拳擊等級中少于112磅體重的等級名稱來命名。享元(“Flyweight”英語中的輕量級)的名稱本身是從以幫以助我們完成減少重量(內(nèi)存標(biāo)記)為目標(biāo)的重量等級推導(dǎo)出的。

        實際應(yīng)用中,輕量級的數(shù)據(jù)共享采集被多個對象使用的相似對象或數(shù)據(jù)結(jié)構(gòu),并將這些數(shù)據(jù)放置于單個的擴(kuò)展對象中。我們可以把它傳遞給依靠這些數(shù)據(jù)的對象,而不是在他們每個上面都存儲一次。

        使用享元

        有兩種方法來使用享元。第一種是數(shù)據(jù)層,基于存儲在內(nèi)存中的大量相同對象的數(shù)據(jù)共享的概念。第二種是DOM層,享元模式被作為事件管理中心,以避免將事件處理程序關(guān)聯(lián)到我們需要相同行為父容器的所有子節(jié)點(diǎn)上。享元模式通常被更多的用于數(shù)據(jù)層,我們先來看看它。

        享元和數(shù)據(jù)共享

        對于這個應(yīng)用程序而言,圍繞經(jīng)典的享元模式有更多需要我們意識到的概念。享元模式中有一個兩種狀態(tài)的概念——內(nèi)在和外在。內(nèi)在信息可能會被我們的對象中的內(nèi)部方法所需要,它們絕對不可以作為功能被帶出。外在信息則可以被移除或者放在外部存儲。

        帶有相同內(nèi)在數(shù)據(jù)的對象可以被一個單獨(dú)的共享對象所代替,它通過一個工廠方法被創(chuàng)建出來。這允許我們?nèi)ワ@著降低隱式數(shù)據(jù)的存儲數(shù)量。

        個中的好處是我們能夠留心于已經(jīng)被初始化的對象,讓只有不同于我們已經(jīng)擁有的對象的內(nèi)在狀態(tài)時,新的拷貝才會被創(chuàng)建。

        我們使用一個管理器來處理外在狀態(tài)。如何實現(xiàn)可以有所不同,但針對此的一種方法就是讓管理器對象包含一個存儲外在狀態(tài)以及它們所屬的享元對象的中心數(shù)據(jù)庫。

        經(jīng)典的享元實現(xiàn)

        近幾年享元模式已經(jīng)在Javascript中得到了深入的應(yīng)用,我們會用到的許多實現(xiàn)方式其靈感來自于Java和C++的世界。

        我們來看下來自維基百科的針對享元模式的 Java 示例的 Javascript 實現(xiàn)。

        在這個實現(xiàn)中我們將要使用如下所列的三種類型的享元組件:

        • 享元對應(yīng)的是一個接口,通過此接口能夠接受和控制外在狀態(tài)。

        • 構(gòu)造享元來實際的實際的實現(xiàn)接口,并存儲內(nèi)在狀態(tài)。構(gòu)造享元須是能夠被共享的,并且具有操作外在狀態(tài)的能力。

        • 享元工廠負(fù)責(zé)管理享元對象,并且也創(chuàng)建它們。它確保了我們的享元對象是共享的,并且可以對其作為一組對象進(jìn)行管理,這一組對象可以在我們需要的時候查詢其中的單個實體。如果一個對象已經(jīng)在一個組里面創(chuàng)建好了,那它就會返回該對象,否則它會在對象池中新創(chuàng)建一個,并且返回之。

        這些對應(yīng)于我們實現(xiàn)中的如下定義:

        • CoffeeOrder:享元

        • CoffeeFlavor:構(gòu)造享元

        • CoffeeOrderContext:輔助器

        • CoffeeFlavorFactory:享元工廠

        • testFlyweight:對我們享元的使用

        鴨式?jīng)_減的 “implements”

        鴨式?jīng)_減允許我們擴(kuò)展一種語言或者解決方法的能力,而不需要變更運(yùn)行時的源。由于接下的方案需要使用一個Java關(guān)鍵字“implements”來實現(xiàn)接口,而在Javascript本地看不到這種方案,那就讓我們首先來對它進(jìn)行鴨式?jīng)_減。

        Function.prototype.implementsFor 在一個對象構(gòu)造器上面起作用,并且將接受一個父類(函數(shù)—)或者對象,而從繼承于普通的繼承(對于函數(shù)而言)或者虛擬繼承(對于對象而言)都可以。

        // Simulate pure virtual inheritance/"implement" keyword for JS Function.prototype.implementsFor = function( parentClassOrObject ){    if ( parentClassOrObject.constructor === Function ) {        // Normal Inheritance        this.prototype = new parentClassOrObject();         this.prototype.constructor = this;         this.prototype.parent = parentClassOrObject.prototype;    } else {        // Pure Virtual Inheritance        this.prototype = parentClassOrObject; this.prototype.constructor = this; this.prototype.parent = parentClassOrObject;    }    return this;};

        我們可以通過讓一個函數(shù)明確的繼承自一個接口來彌補(bǔ)implements關(guān)鍵字的缺失。下面,為了使我們得以去分配支持一個對象的這些實現(xiàn)的功能,CoffeeFlavor實現(xiàn)了CoffeeOrder接口,并且必須包含其接口的方法。

        let CoffeeOrder = {    // Interfaces    serveCoffee:function(context){},    getFlavor:function(){}};function CoffeeFlavor( newFlavor ){    let flavor = newFlavor;    if( typeof this.getFlavor === "function" ){      this.getFlavor = function() {          return flavor;      };    }    if( typeof this.serveCoffee === "function" ){      this.serveCoffee = function( context ) {        console.log("Serving Coffee flavor "+ flavor+" to table number "+ context.getTable());      };    }}CoffeeFlavor.implementsFor( CoffeeOrder );function CoffeeOrderContext( tableNumber ) {   return{      getTable: function() {         return tableNumber;     }   };}function CoffeeFlavorFactory() {    let flavors = {},    length = 0;    return {        getCoffeeFlavor: function (flavorName) {            let flavor = flavors[flavorName];            if (flavor === undefined) {                flavor = new CoffeeFlavor(flavorName);                flavors[flavorName] = flavor;                length++;            }            return flavor;        },        getTotalCoffeeFlavorsMade: function () {            return length;        }    };}function testFlyweight(){  let flavors = new CoffeeFlavor(),    tables = new CoffeeOrderContext(),    ordersMade = 0,    flavorFactory;  function takeOrders( flavorIn, table) {     flavors[ordersMade] = flavorFactory.getCoffeeFlavor( flavorIn );     tables[ordersMade++] = new CoffeeOrderContext( table );  }   flavorFactory = new CoffeeFlavorFactory();   takeOrders("Cappuccino", 2);   takeOrders("Cappuccino", 2);   takeOrders("Frappe", 1);   takeOrders("Frappe", 1);   takeOrders("Xpresso", 1);   takeOrders("Frappe", 897);   takeOrders("Cappuccino", 97);   takeOrders("Cappuccino", 97);   takeOrders("Frappe", 3);   takeOrders("Xpresso", 3);   takeOrders("Cappuccino", 3);   takeOrders("Xpresso", 96);   takeOrders("Frappe", 552);   takeOrders("Cappuccino", 121);   takeOrders("Xpresso", 121);   for (var i = 0; i < ordersMade; ++i) {       flavors[i].serveCoffee(tables[i]);   }   console.log("total CoffeeFlavor objects made: " +  flavorFactory.getTotalCoffeeFlavorsMade());}

        轉(zhuǎn)換代碼為使用享元模式

        接下來,讓我們通過實現(xiàn)一個管理一個圖書館中所有書籍的系統(tǒng)來繼續(xù)觀察享元。分析得知每一本書的重要元數(shù)據(jù)如下:

        • ID

        • 標(biāo)題

        • 作者

        • 類型

        • 總頁數(shù)

        • 出版商ID

        • ISBN

        我們也將需要下面一些屬性,來跟蹤哪一個成員是被借出的一本特定的書,借出它們的日期,還有預(yù)計的歸還日期。

        • 借出日期

        • 借出的成員

        • 規(guī)定歸還時間

        • 可用性

        let Book = function( id, title, author, genre, pageCount,publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate,availability ){   this.id = id;   this.title = title;   this.author = author;   this.genre = genre;   this.pageCount = pageCount;   this.publisherID = publisherID;   this.ISBN = ISBN;   this.checkoutDate = checkoutDate;   this.checkoutMember = checkoutMember;   this.dueReturnDate = dueReturnDate;   this.availability = availability;};
        Book.prototype = { getTitle: function () { return this.title; }, getAuthor: function () { return this.author; }, getISBN: function (){ return this.ISBN; }, updateCheckoutStatus: function( bookID, newStatus, checkoutDate , checkoutMember, newReturnDate ){ this.id = bookID; this.availability = newStatus; this.checkoutDate = checkoutDate; this.checkoutMember = checkoutMember; this.dueReturnDate = newReturnDate; }, extendCheckoutPeriod: function( bookID, newReturnDate ){ this.id = bookID; this.dueReturnDate = newReturnDate; }, isPastDue: function(bookID){ let currentDate = new Date(); return currentDate.getTime() > Date.parse( this.dueReturnDate ); }};

        這對于最初小規(guī)模的藏書可能工作得還好,然而當(dāng)圖書館擴(kuò)充至每一本書的多個版本和可用的備份,這樣一個大型的庫存,我們會發(fā)現(xiàn)管理系統(tǒng)的運(yùn)行隨著時間的推移會越來越慢。使用成千上萬的書籍對象可能會壓倒內(nèi)存,而我們可以通過享元模式的提升來優(yōu)化我們的系統(tǒng)。

        現(xiàn)在我們可以像下面這樣將我們的數(shù)據(jù)分離成為內(nèi)在和外在的狀態(tài):同書籍對象(標(biāo)題,版權(quán)歸屬)相關(guān)的數(shù)據(jù)是內(nèi)在的,而借出數(shù)據(jù)(借出成員,規(guī)定歸還日期)則被看做是外在的。這實際上意味著對于每一種書籍屬性的組合僅需要一個書籍對象。這仍然具有相當(dāng)大的數(shù)量,但相比之前已經(jīng)得到大大的縮減了。

        下面的書籍元數(shù)據(jù)組合的單一實體將在所有帶有一個特定標(biāo)題的書籍拷貝中共享。

        let Book = function ( title, author, genre, pageCount, publisherID, ISBN ) {    this.title = title;    this.author = author;    this.genre = genre;    this.pageCount = pageCount;    this.publisherID = publisherID;    this.ISBN = ISBN;};

        如我們所見,外在狀態(tài)已經(jīng)被移除了。從圖書館借出所要做的一切都被轉(zhuǎn)移到一個管理器中,由于對象數(shù)據(jù)現(xiàn)在是分段的,工廠可以被用來做實例化。

        一個基本工廠

        現(xiàn)在讓我們定義一個非常基本的工廠。我們用它做的工作是,執(zhí)行一個檢查來看看一本給定標(biāo)題的書是不是之前已經(jīng)在系統(tǒng)內(nèi)創(chuàng)建過了;如果創(chuàng)建過了,我們就返回它 - 如果沒有,一本新書就會被創(chuàng)建并保存,使得以后可以訪問它。

        這確保了為每一條本質(zhì)上唯一的數(shù)據(jù),我們只創(chuàng)建了一份單一的拷貝:

        let BookFactory = (function () {  let existingBooks = {}, existingBook;  return {    createBook: function ( title, author, genre, pageCount, publisherID, ISBN ) {      existingBook = existingBooks[ISBN];      if ( !!existingBook ) {        return existingBook;      } else {        let book = new Book( title, author, genre, pageCount, publisherID, ISBN );        existingBooks[ISBN] = book;        return book;      }    }  };});

        管理外在狀態(tài)

        下一步,我們需要將那些從Book對象中移除的狀態(tài)存儲到某一個地方——幸運(yùn)的是一個管理器(我們會將其定義成一個單例)可以被用來封裝它們。書籍對象和借出這些書籍的圖書館成員的組合將被稱作書籍借出記錄。

        這些我們的管理器都將會存儲,并且也包含我們在對Book類進(jìn)行享元優(yōu)化期間剝離的同借出相關(guān)的邏輯。

        let BookRecordManager = (function () {  let bookRecordDatabase = {};  return {    addBookRecord: function ( id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability ) {      let book = bookFactory.createBook( title, author, genre, pageCount, publisherID, ISBN );      bookRecordDatabase[id] = {        checkoutMember: checkoutMember,        checkoutDate: checkoutDate,        dueReturnDate: dueReturnDate,        availability: availability,        book: book      };    },    updateCheckoutStatus: function ( bookID, newStatus, checkoutDate, checkoutMember, newReturnDate ) {      let record = bookRecordDatabase[bookID];      record.availability = newStatus;      record.checkoutDate = checkoutDate;      record.checkoutMember = checkoutMember;      record.dueReturnDate = newReturnDate;    },    extendCheckoutPeriod: function ( bookID, newReturnDate ) {      bookRecordDatabase[bookID].dueReturnDate = newReturnDate;    },    isPastDue: function ( bookID ) {      let currentDate = new Date();      return currentDate.getTime() > Date.parse( bookRecordDatabase[bookID].dueReturnDate );    }  };});

        這些改變的結(jié)果是所有從Book類中擷取的數(shù)據(jù)現(xiàn)在被存儲到了BookManager單例(BookDatabase)的一個屬性之中——與我們以前使用大量對象相比可以被認(rèn)為是更加高效的東西。同書籍借出相關(guān)的方法也被設(shè)置在這里,因為它們處理的數(shù)據(jù)是外在的而不內(nèi)在的。

        這個過程確實給我們最終的解決方法增加了一點(diǎn)點(diǎn)復(fù)雜性,然而同已經(jīng)明智解決的數(shù)據(jù)性能問題相比,這只是一個小擔(dān)憂,如果我們有同一本書的30份拷貝,現(xiàn)在我們只需要存儲它一次就夠了。

        每一個函數(shù)也會占用內(nèi)存。使用享元模式這些函數(shù)只在一個地方存在(就是在管理器上),并且不是在每一個對象上面,這節(jié)約了內(nèi)存上的使用。

        本文完?


        瀏覽 46
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            久久日韩精品一区二区 | 精品国产 国产没封 | 性一交一乱一交一乱A片96 | 色综合色狠狠天天综合色 | 日韩日比视频 | 成人片全裸直播在线观看视频 | 国产又粗又硬又猛的免费视频 | 高清乱码 毛片 | 大香蕉乱伦 | 夜夜精品视频一区二区 |