由于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模式相比生成子类更灵活,在不改变接口的前提下可以增强类的功能,在如下场景可以考虑使用装饰模式:
- 需要扩展一个类的功能,或给一个类增加附加责任
- 动态地给一个对象增加功能,这些功能可以再动态撤销
- 需要增加一些基本功能的排列组合而产生的非常大量的功能,从而使继承变得 不现实
装饰模式下存在以下几个角色:
- 抽象构件:给出一个抽象接口,以规范准备接收附加责任的对象
- 具体构件:定义一个将要接收附加责任的类
- 装饰角色:持有一个构件对象的实例,并定一个与抽象构件一致的接口
- 具体装饰角色:负责给构件对象添加附加责任
相关概念可参考:设计模式——装饰模式(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() );