設計模式JavaScript實現

前端閱讀室

GoF合做出版的《設計模式》這本書提供了許多有關與面向對象軟件設計中常見問題的解決方案。這些模式已經出現了至關長的一段時間,已經被證實在許多狀況下都很是有用。前端

單體模式

一個特定類僅有一個實例。這意味着當您第二次使用同一個類建立新對象的時候,應該獲得與第一次所建立對象徹底相同對象。算法

使用對象字面量建立一個簡單的對象也是一個單體的例子,由於在JavaScript中沒有類,只有對象。當您建立一個新對象時,實際上沒有其餘對象與其相似,所以新對象已是單體了。編程

var obj = {
  myprop: 'my value'
};
複製代碼

使用new操做符

JavaScript沒有類,可是能夠經過new語法使用構造函數來建立對象,有時有可能須要使用這種語法的單體實現。這種思想在於當使用同一個構造函數以new操做符來建立多個對象時,應該僅得到指向徹底相同的對象的新指針。設計模式

靜態屬性中的實例

在構造函數的靜態屬性中緩存該實例。您可使用相似Universe.instance的屬性並將實例緩存在該屬性中。這中方案的缺點在於instance屬性是公開可訪問的屬性,在外部代碼中可能會修改該屬性。數組

function Universe() {
  if (typeof Universe.instance === 'object') {
    return Universe.instance;
  }

  this.start_time = 0;

  Universe.instance = this;
}

// 測試
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true
複製代碼

閉包中的實例

能夠將該實例包裝在閉包中。這樣能夠保證該實例的私有性。其代價是帶來了額外的閉包開銷。瀏覽器

function Universe() {
  var instance = this;

  this.start_time = 0;

  Universe = function () {
    return this;
  };
}
複製代碼

若是須要使原型和構造函數指針按照預期的那樣運行,能夠經過作一些調整來實現這個目標:緩存

function Universe() {
  var instance;

  Universe = function Universe() {
    return instance;
  };

  Universe.prototype = this;

  instance = new Universe();

  instance.constructor = Universe;

  instance.start_time = 0;

  return instance;
}
複製代碼

另外一種解決方案也是將構造函數和實例包裝在即時函數中。安全

var Universe;

(function () {
  var instance;

  Universe = function Universe() {
    if (instance) {
      return instance;
    }
    instance = this;

    this.start_time = 0;
  };
}());
複製代碼

工廠模式

設計工廠模式的目的是爲了建立對象。它一般在類或者類的靜態方法中實現,具備下列目標:數據結構

  1. 當建立類似對象時執行重複操做
  2. 在編譯時不知道具體類型(類)的狀況下,爲工廠客戶提供一種建立對象的接口。

經過工廠方法(或類)建立的對象在設計上都繼承了相同的父對象這個思想,它們都是實現專門功能的特定子類。有時候公共父類是一個包含了工廠方法的同一個類。閉包

下面是工廠模式的實現示例

function CarMaker() {}

CarMaker.prototype.drive = function () {
  return this.doors;
};

CarMaker.factory = function (type) {
  var constr = type,
      newcar;
  
  if (typeof CarMaker[constr] !== 'function') {
    throw {
      name: "Error",
      message: constr + " not exist"
    };
  }
  if (typeof CarMaker[constr].prototype.drive !== 'function') {
    CarMaker[constr].prototype = new CarMaker();
  }
  newcar = new CarMaker[constr]();
  return newcar;
};

CarMarker.Compact = function () {
  this.doors = 4;
};
CarMarker.Convertible = function () {
  this.doors = 2;
};
CarMarker.SUV = function () {
  this.doors = 24;
};
複製代碼

內置對象工廠

var o = new Object(),
    n = new Object(),
    s = Object('1'),
    b = Object(true);

o.constructor === Object;
n.constructor === Number;
s.constructor === String;
b.constructor === Boolean;
// 都爲true
複製代碼

迭代器模式

在迭代器模式中,一般有一個包含某種數據集合的對象。該數據可能存儲在一個複雜數據結構內部,而要提供一種簡單的方法可以訪問數據結構中每一個元素。對象的消費者並不須要知道如何組織數據,全部須要作的就是取出單個數據進行工做。

在迭代器模式中,對象須要提供一個next()方法。依次調用next()必須返回下一個連續的元素。固然,在特定數據結構中,"下一個"所表明的意義是由您來決定的。

示例

var agg = (function () {
  var index = 0,
      data = [1, 2, 3, 4, 5],
      length = data.length;

  return {
    next: function () {
      var element;
      if (!this.hasNext()) {
        return null;
      }
      element = data[index];
      index = index + 2;
      return element;
    },
    hasNext: function () {
      return index < length;
    }
  };
}());
複製代碼

裝飾者模式

在裝飾者模式中,能夠在運行時動態添加附加功能到對象中。裝飾者模式的一個比較方便的特徵在於其預期行爲的可定製和可配置特性。能夠從僅具備一些基本功能的普通對象開始,而後從可用裝飾資源池中選擇須要用於加強普通對象的那些功能,而且按照順序進行裝飾,尤爲是當裝飾順序很重要的時候。

經過原型鏈繼承實現

function Sale(price) {
  this.price = price || 100;
}
Sale.prototype.getPrice = function () {
  return this.price;
};

Sale.decorators.fedtax = {
  getPrice: function () {
    var price = this.uber.getPrice();
    price += price * 5 / 100;
    return price;
  }
};

Sale.decorators.quebec = {
  getPrice: function () {
    var price = this.uber.getPrice();
    price += price * 7.5 / 100;
    return price;
  }
};

Sale.prototype.decorate = function(decorator) {
  var F = function () {},
      overrides = this.constructor.decorators[decorator],
      i, newobj;
  F.prototype = this;
  newobj = new F();
  newobj.uber = F.prototype;
  for (i in overrides) {
    if (overrides.hasOwnProperty(i)) {
      newobj[i] = overrides[i];
    }
  }
  return newobj;
};

// 測試
var sale = new Sale(100);
sale = sale.decorate('fedtax');
sale = sale.decorate('quebec');
sale.getPrice();
複製代碼

使用列表實現

function Sale(price) {
  this.price = (price > 0) || 100;
  this.decorators_list = [];
}

Sale.decorators = {};
Sale.decorators.fedtax = {
  getPrice: function (price) {
    return price + price * 5 / 100;
  }
};
Sale.decorators.quebec = {
  getPrice: function (price) {
    return price + price * 7.5 / 100;
  }
};

Sale.prototype.decorate = function (decorator) {
  this.decorators_list.push(decorator);
};

Sale.prototype.getPrice = function () {
  var price = this.price,
      i,
      max = this.decorators_list.length,
      name;
  for (i = 0; i < max; i += 1) {
    name = this.decorators_list[i];
    price = Sale.decorators[name].getPrice(price);
  }
  return price;
}

// 測試
var sale = new Sale(100);
sale.decorate('fedtax');
sale.decorate('quebec');
sale.getPrice();
複製代碼

策略模式

策略模式支持您在運行時選擇算法。代碼的客戶端可使用同一個接口來工做,可是它卻根據客戶正在試圖執行任務的上下文,從多個算法中選擇用於處理特定任務的算法。

數據驗證示例

var validator = {
  types: {},

  messages: [],

  config: {},

  validate: function (data) {
    var i, msg, type, checker, result_ok;

    this.messages = [];

    for (i in data) {
      if (data.hasOwnProperty(i)) {
        type = this.config[i];
        checker = this.types[type];

        if (!type) {
          continue;
        }
        if (!checker) {
          throw {
            name: 'ValidationError',
            message: 'No handler to validate type ' + type;
          };
        }

        result_ok = checker.validate(data[i]);
        if (!result_ok) {
          msg = "Invalid value for *" + i + "*, " + checker.instructions;
          this.messages.push(msg);
        }
      }
    }
    return this.hasErrors();
  },
  hasErrors: function () {
    return this.message.length !== 0;
  }


};

validator.types.isNonEmpty = {
  validate: function (value) {
    return value !== "";
  },
  instructions: "this value cannot be empty"
};

validator.types.isNumber = {
  validate: function (value) {
    return !isNaN(value);
  },
  instructions: "this value can only be a valid number, e.g. 1, 3.14 or 2010"
};

// 測試
var data = {
  first_name: "Super",
  age: "unknown",
};
validator.config = {
  first_name: 'isNonEmpty',
  age: 'isNumber',
};
validator.validate(data);
if (validator.hasErrors()) {
  console.log(validator.message.join("\n"));
}
複製代碼

外觀模式

外觀模式是一種簡單的模式,它爲對象提供了一個可供選擇的接口。這是一種很是好的設計實踐,可保持方法的簡潔性而且不會使它們處理過多的工做。若是原來有許多接受多個參數的uber方法,相比而言,按照本實現方法,最終將會建立更多數量的方法。有時候,兩個或更多的方法可能廣泛的被一塊兒調用。在這樣的狀況下,建立另外一個方法以包裝重複的方法調用是很是有意義的。

外觀模式很是適合於瀏覽器腳本處理,據此可將瀏覽器之間的差別隱藏在外觀以後。

var myevent = {
  stop: function (e) {
    if (typeof e.preventDefault === 'function') {
      e.preventDefault();
    }
    if (typeof e.stopPropagation === 'function') {
      e.stopPropagation();
    }
    if (typeof e.returnValue === 'boolean') {
      e.returnValue = false;
    }
    if (typeof e.cancelBubble === 'boolean') {
      typeof e.cancelBubble = true;
    }
  }
};
複製代碼

代理模式

在代理設計模式中,一個對象充當另外一個對象的接口。代理介於對象的客戶端和對象自己之間,而且對該對象的訪問進行保護。

使用這種模式的其中一個例子是咱們能夠稱爲延遲初始化的方法,代理接收初始化請求,可是直到該本體對象明確的將被實際使用以前,代理從不會將該請求傳遞給本體對象。

範例(略)

  1. 經過代理合並多個http請求以提升性能

  2. 緩存代理

中介者模式

應用程序,不管其大小,都是由一些單個的對象所組成。全部這些對象須要一種方式來實現相互通訊,而這種通訊方式在必定程度上不下降可維護性,也不損害那種安全的改變部分應用程序而不會破壞其他部分的能力。隨着應用程序的增加,將添加愈來愈多的對象。而後在代碼重構期間,對象將被刪除或從新整理。當對象相互知道太多信息而且直接通訊(調用對方的方法並改變屬性)時,這將致使產生不良的緊耦合問題。

中介者模式緩解了該問題並促進造成鬆耦合,在這種模式中,獨立的對象之間並不直接通訊,而是經過mediator對象。當其中一個colleague對象改變狀態之後,它將會通知該mediator,而mediator將會把該變化傳達到任意其餘應該知道此變化的colleague對象。

中介者示例

function Player(name) {
  this.points = 0;
  this.name = name;
}

Player.prototype.play = function () {
  this.points += 1;
  mediator.played();
}

var scoreboard = {
  element: document.getElementById('results');

  update: function (score) {
    var i, msg = '';
    for (i in score) {
      if (score.hasOwnProperty(i)) {
        msg += i + ': ' + score[i] + ' | '
      }
    }
    this.element.innerHTML = msg;
  }
};

var mediator = {
  players: {},
  setup: function () {
    var players = this.players;
    players.home = new Player('Home');
    players.guest = new Player('Guest');
  },
  played: function () {
    var players = this.players,
    score = {
      Home: players.home.points,
      Guest: players.guest.points
    };
    scoreboard.update(score);
  },
  keypress: function (e) {
    e = e || window.event;
    if (e.whitch === 49) {
      mediator.players.home.play();
      return;
    }
    if (e.whitch === 48) {
      mediator.players.guest.play();
      return;
    }
  }
};
// 測試
mediator.setup();
window.onkeypress = mediator.keypress;

setTimeout(function () {
  window.onkeypress = null;
  alert('Game over!');
}, 30000);
複製代碼

觀察者模式

觀察者模式普遍應用於客戶端JavaScript編程中。全部的瀏覽器事件是該模式的例子。它也叫訂閱/發佈模式。

設計這種模式背後的主要動機是促進造成鬆耦合。在這種模式中,並非一個對象調用另外一個對象的方法,而是一個對象訂閱另外一個對象的特定活動並在狀態改變後得到通知。訂閱者也稱之爲觀察者,而被觀察者的對象稱爲發佈者或者主題。當發生了一個重要的事件時,發佈者將會通知(調用)全部訂閱者而且可能常常以事件對象的形式傳遞消息。

示例:雜誌訂閱

咱們要實現一個功能:發佈者paper,天天出版報紙以及月刊雜誌,訂閱者joe被通知發生的新聞。

下面是通用發佈者功能的一個實現示例

var publisher = {
  subscribers: {
    any: [] // 事件類型:訂閱者(subscribers)
  },
  // 將訂閱者添加到subscribers數組
  subscribe: function (fn, type) {
    type = type || 'any';
    if (typeof this.subscribers[type] === 'undefined') {
      this.subscribers[type] = [];
    }
    this.subscribers[type].push(fn);
  },
  // 從訂閱者數組subscribers中刪除訂閱者
  unsubscribe: function (fn, type) {
    this.visitSubscribers('unsubscribe', fn, type);
  },
  // 循環遍歷subscribers中的每一個元素,而且調用他們註冊時提供的方法
  publish: function (publication, type) {
    this.visitSubscribers('publish', publication, type);
  },
  visitSubscribers: function (action, arg, type) {
    var pubtype = type || 'any',
        subscribers = this.subscribers[pubtype],
        i,
        max = subscribers.length;

    for (i = 0; i < max; i += 1) {
      if (action === 'publish') {
        subscribers[i](arg);
      } else {
        if (subscribers[i] === arg) {
          subscribers.splice(i, 1);
        }
      }
    }
  }
};
複製代碼

將普通對象轉換成發佈者對象

function makePublisher(o) {
  var i;
  for (i in publisher) {
    if (publisher.hasOwnProperty(i) && typeof publisher[i] === 'function') {
      o[i] = publisher[i];
    }
  }
  o.subscribers = { any: [] };
}
複製代碼

功能實現:

paper對象

var paper = {
  daily: function () {
    this.publish("big news totay");
  },
  monthly: function () {
    this.publish("interesting analysis", "monthly");
  }
};
複製代碼

將paper構形成一個發佈者

makePublisher(paper);
複製代碼

訂閱者對象joe

var joe = {
  drinkCoffee: function (paper) {
    console.log('Just read ' + paper);
  },
  sundayPreNap: function (monthly) {
    console.log('About to fall asleep reading this' + monthly);
  }
};
複製代碼

joe向paper訂閱

paper.subscribe(joe.drinkCoffee);
paper.subscribe(joe.sundayPreNap, 'monthly');
複製代碼

觸發一些事件

paper.daily();
paper.monthly();
複製代碼

打印結果

Just read big news totay
About to fall asleep reading this interesting analysis
複製代碼

代碼好的部分在於,paper對象沒有硬編碼joe,joe對象也沒有硬編碼paper。此外,代碼中也沒有那些知道一切的中介者對象。參與對象是鬆耦合的,咱們能夠向paper添加更多的訂閱者而不須要修改這些對象。

讓咱們將例子更進一步擴展而且讓joe成爲發佈者:

makePublisher(joe);
joe.tweet = function (msg) {
  this.publish(msg);
};
複製代碼

選擇paper的公關部門須要讀取讀者的tweet,而且訂閱joe的信息,那麼須要提供方法readTweets():

paper.readTweets = function (tweet) {
  console.log('Call big meeting! Someone ' + tweet);
};
joe.subscribe(paper.readTweets);
複製代碼

執行

joe.tweet('hated the paper today');
複製代碼

打印

Call big meeting! Someone hated the paper today
複製代碼

參考

  1. 《JavaScript模式》

前端閱讀室
相關文章
相關標籤/搜索