由于JavaScript不是典型的面向對象語言,因而在實現一些經典的設計模式上也與一般語言存在差異,本文主要介紹在JavaScript中如何實現常用的設計模式。
1. 單例模式
單例模式是最常見的設計模式,在一般的OOP語言中,我們可以通過私有化構造函數實現單例模式。但由于單例模式實際上可以看做返回的是一個結構,該結構在內存中有且僅有唯一的一份,所以可以類比JavaScript中的閉包,所以可以記住閉包完成單例模式的實現:
// 單例模式
var mySingleton = (function(){
var instance;
init = function() {
var privateVar = "privateVar";
privateFunc = function() {
console.log("This is private func");
};
return {
publicVar: 'public var', // 公共變量
publicFunc: function() { // 公共方法
console.log('This is public func');
},
getPrivateVar: function() {
return privateVar;
}
}
};
return {
getInstance: function() {
if (!instance) {
instance = init();
}
return instance;
}
}
})();
var singleton1 = mySingleton.getInstance();
var singleton2 = mySingleton.getInstance();
singleton1.publicFunc();
console.log(singleton1 === singleton2);
2. 觀察者模式
觀察者模式下存在兩個成員:觀察者和被觀察者。觀察者在被被觀察者處進行注冊,當被觀察者相關狀態發生變化時,被觀察者告知觀察者,同時觀察者執行相應更新邏輯。通常來說,存在多個觀察者觀察同一個被觀察者的情況。在觀察者模式下,存在以下幾個組件:
- 被觀察者:維護一組被觀察接口,用于添加、刪除觀察者,通知觀察者
- 觀察者:維護一組觀察者接口,用于在被觀察者狀態發生變化時,通知到觀察者
- 具體的被觀察者:實現被觀察者接口
- 具體的觀察者:實現觀察者接口
// 觀察者模式:建立觀察者/被觀察者關系,觀察者可以注冊其觀察對象(被觀察者),當被觀察者的狀態發生改變時,可以及時通知到觀察者
// 被觀察者管理觀察者能力建模
function ObserverList() {
this.observerList = [];
}
// 添加觀察者
ObserverList.prototype.Add = function(observer) {
this.observerList.push(observer);
}
// 清空觀察者
ObserverList.prototype.Empty = function() {
this.observerList = [];
}
// 觀察者數量
ObserverList.prototype.Count = function() {
return this.observerList.length;
}
// 獲取某個觀察者
ObserverList.prototype.Get = function(index) {
if (index >= 0 && index < this.observerList.length) {
return this.observerList[index];
}
return undefined;
}
// 刪除某個觀察者
ObserverList.prototype.RemoveAt = function( index ){
if( index === 0 ){
this.observerList.shift();
}else if( index === this.observerList.length -1 ){
this.observerList.pop();
}
};
// var testObserverList = new ObserverList();
// for(var key in testObserverList) {
// console.log('key:' + key + '->' + testObserverList[key]);
// }
// 給某個對象擴展被觀察者能力
function extend(extension, target) {
for(var key in extension) {
target[key] = extension[key];
}
}
// 創建被觀察者對象Subject,同時集成觀察者對象的能力
function Subject() {
this.observerList = new ObserverList();
};
Subject.prototype.AddObserver = function(observer) {
this.observerList.Add(observer)
};
Subject.prototype.RemoveObserver = function( observer ){
this.observers.RemoveAt( this.observers.IndexOf( observer, 0 ) );
};
// 通知所有觀察者
Subject.prototype.Notify = function(context) {
var count = this.observerList.Count();
for(var i = 0; i < count; i++) {
this.observerList.Get(i).Update(context);
}
};
// 構建觀察者對象,主要是定義觀察后的處理函數
function Observer() {
this.Update = function() {
//do something
}
}
接下來我們基于觀察者模式實現一個例子:
- 一個按鈕,這個按鈕用于增加新的充當觀察者的選擇框到頁面上
- 一個控制器的選擇框,充當一個被觀察者,通知其他選擇框是否應該被選中
- 一個容器,用于放置新的選擇框
<body>
<button id="addNewObserver">Add New Observer checkbox</button>
<input id="mainCheckbox" type="checkbox"/>
<div id="observersContainer"></div>
</body>
<script src="./observer.js"></script> <!-- 引入上文中的js代碼 -->
<script type="text/javascript">
var controlCheckbox = document.getElementById('mainCheckbox');
var addBtn = document.getElementById('addNewObserver');
var container = document.getElementById('observersContainer');
// 給controlCheckbox擴展被觀察者能力
extend(new Subject(), controlCheckbox);
controlCheckbox.addEventListener('click', function() {
this.Notify(this.checked);
});
// 添加觀察者
addBtn.addEventListener('click', AddNewObserver);
function AddNewObserver() {
// 創建一個checkbox
var check = document.createElement('input');
check.type = 'checkbox';
check.checked = controlCheckbox.checked;
// 擴展觀察者能力
extend(new Observer(), check);
check.Update = function(checked) {
this.checked = checked;
}
//添加到controlCheckbox的觀察者列表中
controlCheckbox.AddObserver(check);
// 添加到容器區域
container.appendChild(check);
}
}
3 訂閱模式
訂閱模式和觀察者模式很類似,都是建立觀察者與被觀察者之間的消息通道。觀察者模式需要觀察者顯示的調用被觀察者的觀察接口來聲明觀察關系,從而在代碼層面存在依賴關系。而訂閱模式通過使用主題/事件頻道將訂閱者和發布者進行解耦。
// 訂閱者對象
function Subscriber() {
this.subscriberEventList = [];
}
Subscriber.prototype.addSubscribe = function(subscribe) {
this.subscriberEventList.push(subscribe);
}
// 訂閱事件對象
function Subscribe(name, callback) {
this.name = name;
this.callback = callback;
}
// 發布事件對象
function Publish(name, context) {
this.name = name;
this.context = context;
}
//訂閱中心對象
function SubscribeCenter() {
this.subscriberList = [];
}
SubscribeCenter.prototype.addSubscriber = function(subscriber) {
this.subscriberList.push(subscriber);
}
SubscribeCenter.prototype.publish = function(publisher) {
var name = publisher.name;
var context = publisher.context;
for(var i = 0; i < this.subscriberList.length; i++) {
for(var j = 0; j < this.subscriberList[i].subscriberEventList.length; j++) {
var subscribeevent = this.subscriberList[i].subscriberEventList[j];
if(subscribeevent.name === name) {
subscribeevent.callback.call(this.subscriberList[i], name, context);
}
}
}
}
function extend(extend, obj) {
for(var key in extend) {
obj[key] = extend[key];
}
}
4. 工廠模式
工廠模式的實質由一個工廠類來代理對象(工廠模式下稱為組件)的構造,組件遵循同一套組件接口,使用方只需按照工廠定制的標準將參數傳遞給工廠類的組件構造函數即可。工廠模式實現了組件使用方與組件之間的解耦,使得兩者之間不存在顯示的依賴關系,特別適合于組件眾多的情況。
// 工廠模式
// A constructor for defining new cars
function Car( options ) {
// some defaults
this.doors = options.doors || 4;
this.state = options.state || "brand new";
this.color = options.color || "silver";
}
// A constructor for defining new trucks
function Truck( options){
this.state = options.state || "used";
this.wheelSize = options.wheelSize || "large";
this.color = options.color || "blue";
}
// FactoryExample.js
// Define a skeleton vehicle factory
function VehicleFactory() {}
// Define the prototypes and utilities for this factory
// Our default vehicleClass is Car
VehicleFactory.prototype.vehicleClass = Car;
// Our Factory method for creating new Vehicle instances
VehicleFactory.prototype.createVehicle = function ( options ) {
if( options.vehicleType === "car" ){
this.vehicleClass = Car;
}else{
this.vehicleClass = Truck;
}
return new this.vehicleClass( options );
};
// Create an instance of our factory that makes cars
var carFactory = new VehicleFactory();
var car = carFactory.createVehicle( {
vehicleType: "car",
color: "yellow",
doors: 6 } );
// Test to confirm our car was created using the vehicleClass/prototype Car
// Outputs: true
console.log( car instanceof Car );
// Outputs: Car object of color "yellow", doors: 6 in a "brand new" state
console.log( car );
// 抽象工廠
var AbstractVehicleFactory = (function () {
// Storage for our vehicle types
var types = {};
return {
getVehicle: function ( type, customizations ) {
var Vehicle = types[type];
return (Vehicle ? new Vehicle(customizations) : null);
},
registerVehicle: function ( type, Vehicle ) {
var proto = Vehicle.prototype;
// only register classes that fulfill the vehicle contract
if ( proto.drive && proto.breakDown ) {
types[type] = Vehicle;
}
return AbstractVehicleFactory;
}
};
})();
// Usage:
AbstractVehicleFactory.registerVehicle( "car", Car );
AbstractVehicleFactory.registerVehicle( "truck", Truck );
// Instantiate a new car based on the abstract vehicle type
var car = AbstractVehicleFactory.getVehicle( "car" , {
color: "lime green",
state: "like new" } );
// Instantiate a new truck in a similar manner
var truck = AbstractVehicleFactory.getVehicle( "truck" , {
wheelSize: "medium",
color: "neon yellow" } );
5. Mixin模式
mixin是javascript中最為常用的一種模式,幾乎所有javascript框架都用到了mixin。既可以將任意一個對象的全部和部分屬性拷貝到另一個對象或類上。Mix允許對象以最小量的復雜性從外部借用(或者說繼承)功能.作為一種利用Javascript對象原型工作得很好的模式,它為我們提供了從不止一個Mix處分享功能的相當靈活,但比多繼承有效得多得多的方式。
// Define a simple Car constructor
var Car = function ( settings ) {
this.model = settings.model || "no model provided";
this.color = settings.color || "no colour provided";
};
// Mixin
var Mixin = function () {};
Mixin.prototype = {
driveForward: function () {
console.log( "drive forward" );
},
driveBackward: function () {
console.log( "drive backward" );
},
driveSideways: function () {
console.log( "drive sideways" );
}
};
// Extend an existing object with a method from another
function augment( receivingClass, givingClass ) {
// only provide certain methods
if ( arguments[2] ) {
for ( var i = 2, len = arguments.length; i < len; i++ ) {
receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];
}
}
// provide all methods
else {
for ( var methodName in givingClass.prototype ) {
// check to make sure the receiving class doesn't
// have a method of the same name as the one currently
// being processed
if ( !Object.hasOwnProperty(receivingClass.prototype, methodName) ) {
receivingClass.prototype[methodName] = givingClass.prototype[methodName];
}
// Alternatively:
// if ( !receivingClass.prototype[methodName] ) {
// receivingClass.prototype[methodName] = givingClass.prototype[methodName];
// }
}
}
}
// Augment the Car constructor to include "driveForward" and "driveBackward"
augment( Car, Mixin, "driveForward", "driveBackward" );
// Create a new Car
var myCar = new Car({
model: "Ford Escort",
color: "blue"
});
// Test to make sure we now have access to the methods
myCar.driveForward();
myCar.driveBackward();
// Outputs:
// drive forward
// drive backward
// We can also augment Car to include all functions from our mixin
// by not explicitly listing a selection of them
augment( Car, Mixin );
var mySportsCar = new Car({
model: "Porsche",
color: "red"
});
mySportsCar.driveSideways();
// Outputs:
// drive sideways
6. 裝飾模式
裝飾模式動態地給一個對象增加一些額外的職責。就功能來說,Decorator模式相比生成子類更靈活,在不改變接口的前提下可以增強類的功能,在如下場景可以考慮使用裝飾模式:
- 需要擴展一個類的功能,或給一個類增加附加責任
- 動態地給一個對象增加功能,這些功能可以再動態撤銷
- 需要增加一些基本功能的排列組合而產生的非常大量的功能,從而使繼承變得 不現實
裝飾模式下存在以下幾個角色:
- 抽象構件:給出一個抽象接口,以規范準備接收附加責任的對象
- 具體構件:定義一個將要接收附加責任的類
- 裝飾角色:持有一個構件對象的實例,并定一個與抽象構件一致的接口
- 具體裝飾角色:負責給構件對象添加附加責任
[圖片上傳失敗...(image-7703bf-1512819295760)]
相關概念可參考:設計模式——裝飾模式(Decorator)
// The constructor to decorate
function MacBook() {
this.cost = function () { return 997; };
this.screenSize = function () { return 11.6; };
}
// Decorator 1
function Memory( macbook ) {
var v = macbook.cost();
macbook.cost = function() {
return v + 75;
};
}
// Decorator 2
function Engraving( macbook ){
var v = macbook.cost();
macbook.cost = function(){
return v + 200;
};
}
// Decorator 3
function Insurance( macbook ){
var v = macbook.cost();
macbook.cost = function(){
return v + 250;
};
}
var mb = new MacBook();
Memory( mb );
Engraving( mb );
Insurance( mb );
// Outputs: 1522
console.log( mb.cost() );
// Outputs: 11.6
console.log( mb.screenSize() );