原型模式
为JavaScript设计面向对象系统之初,Brendan Eich 就没有打算在JavaScript中加入类的概念。
在以类为中心的面向对象编程语言中,类和对象的关系可以想象成铸模和铸件的关系,对象总是从类中创建而来。而在原型编程的思想中,类并不是必需的,对象未必需要从类中创建而来, 一个对象是通过克隆另外一个对象所得到的。
原型模式是用于创建对象的一种模式,如果我们想要创建一个对象, 一种方法是先指定它的类型,然后通过类来创建这个对象。原型模式选择了另外一种方式,我们 不再关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象。
原型模式的实现关键,是语言本身是否提供了clone 方法。ECMAScript 5 提供了Object.create
方法,可以用来克隆对象。
但实际上,不需要调用Object.create
,我们在JavaScript 遇到的每个对象,都是从Object.prototype
对象克隆而来的,Object.prototype
就是它们的原型。比如下面的obj1 对象和obj2 对象:
var obj1 = new Object();
var obj2 = {};
复制代码
在JavaScript 语言里,我们并不需要关心克隆的细节,因为这是引擎内部负责实现的。我们所需要做的只是显式地调用var obj1 = new Object() 或者 var obj2 = {}。此时,引擎内部会从 Object.prototype
上面克隆一个对象出来,我们最终得到的就是这个对象。
再来看看如何用new 运算符从构造器中得到一个对象,下面的代码我们再熟悉不过了:
function Person( name ){
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};
var a = new Person( 'sven' )
console.log( a.name ); // 输出:sven
console.log( a.getName() ); // 输出:sven
console.log( Object.getPrototypeOf( a ) === Person.prototype );
复制代码
我们调用了new Person() ,但在这里Person并不是类,而是函数构造器,JavaScript的函数既可以作为普通函数被调用,也可以作为构造器被调用。当使用new 运算符来调用函数时,此时的函数就是一个构造器。
在Chrome 和Firefox 等向外暴露了对象__proto__属性的浏览器下,我们可以通过下面这段代码来理解new运算的过程:
function Person( name ){
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};
var objectFactory = function(){
var obj = new Object(), // 从Object.prototype 上克隆一个空的对象
Constructor = [].shift.call( arguments ); // 取得外部传入的构造器,此例是Person
obj.__proto__ = Constructor.prototype; // 指向正确的原型
var ret = Constructor.apply( obj, arguments ); // 借用外部传入的构造器给obj 设置属性
return typeof ret === 'object' ? ret : obj; // 确保构造器总是会返回一个对象
};
var a = objectFactory( Person, 'sven' );
console.log( a.name ); // 输出:sven
console.log( a.getName() ); // 输出:sven
console.log( Object.getPrototypeOf( a ) === Person.prototype ); // 输出:true
复制代码
对于原型,我们很清楚的一点是当一个对象无法响应某个请求的时候,它会顺着原型链把请求传递下去,直到遇到一个可以处理该请求的对象为止。
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点
class Demo {
}
function getInstance() {
let instance = null;
return function () {
if (!instance) {
instance = new Demo();
}
return instance;
}
}
const instance1 = getInstance();
const instance2 = getInstance();
策略模式
用于替换很多
if
else
的场景
改造前
const calculateBonus = function (performanceLevel, salary) {
if (performanceLevel === 'S') {
return salary * 4;
}
if (performanceLevel === 'A') {
return salary * 3;
}
if (performanceLevel === 'B') {
return salary * 2;
}
};
console.log(calculateBonus('B', 20000))
console.log(calculateBonus('S', 6000))
改造后
const strategies = {
"S": function (salary) {
return salary * 4;
},
"A": function (salary) {
return salary * 3;
},
"B": function (salary) {
return salary * 2;
}
};
const calculateBonus = function (level, salary) {
return strategies[level](salary);
};
console.log(calculateBonus('S', 20000))
console.log(calculateBonus('A', 10000))
状态模式
状态模式
的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
我们来想象这样一个场景:有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同的状态下,表现出来的行为是不一样的。
首先给出不用状态模式的电灯程序实现:
var Light = function () {
this.state = 'off'; // 给电灯设置初始状态off
this.button = null; // 电灯开关按钮
};
Light.prototype.init = function () {
var button = document.createElement('button'),
self = this;
button.innerHTML = '开关';
this.button = document.body.appendChild(button);
this.button.onclick = function () {
self.buttonWasPressed();
}
};
Light.prototype.buttonWasPressed = function () {
if (this.state === 'off') {
console.log('开灯');
this.state = 'on';
} else if (this.state === 'on') {
console.log('关灯');
this.state = 'off';
}
};
var light = new Light();
light.init();
复制代码
令人遗憾的是,这个世界上的电灯并非只有一种。许多酒店里有另外一种电灯,这种电灯也只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯。而这些必须改造上面的代码来完成这种新型电灯的制造。
但将状态改变的逻辑全部写在buttonWasPressed中存在以下问题:
- 违反开放-封闭原则的,每次新增或者修改light的状态,都需要改动buttonWasPressed方法中的代码。
- 所有跟状态有关的行为,都被封装在buttonWasPressed方法里,我们将无法预计这个方法将膨胀到什么地步。
- 状态的切换非常不明显,仅仅表现为对state变量赋值,我们也没有办法一目了然地明白电灯一共有多少种状态。
- 状态之间的切换关系,不过是往buttonWasPressed方法里堆砌if、else语句,这使buttonWasPressed更加难以阅读和维护。
现在我们学习使用状态模式改进电灯的程序。状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以button被按下的的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。
// OffLightState:
var OffLightState = function (light) {
this.light = light;
};
OffLightState.prototype.buttonWasPressed = function () {
console.log('弱光'); // offLightState 对应的行为
this.light.setState(this.light.weakLightState); // 切换状态到weakLightState
};
// WeakLightState:
var WeakLightState = function (light) {
this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function () {
console.log('强光'); // weakLightState 对应的行为
this.light.setState(this.light.strongLightState); // 切换状态到strongLightState
};
// StrongLightState:
var StrongLightState = function (light) {
this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function () {
console.log('关灯'); // strongLightState 对应的行为
this.light.setState(this.light.offLightState); // 切换状态到offLightState
};
var Light = function () {
this.offLightState = new OffLightState(this);
this.weakLightState = new WeakLightState(this);
this.strongLightState = new StrongLightState(this);
this.button = null;
};
Light.prototype.init = function () {
var button = document.createElement('button'),
self = this;
this.button = document.body.appendChild(button);
this.button.innerHTML = '开关';
this.currState = this.offLightState; // 设置当前状态
this.button.onclick = function () {
self.currState.buttonWasPressed();
}
};
Light.prototype.setState = function (newState) {
this.currState = newState;
};
var light = new Light();
light.init();
复制代码
执行结果跟之前的代码一致,但是使用状态模式的好处很明显,它可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中,便于阅读和管理代码。
另外,状态之间的切换都被分布在状态类内部,这使得我们无需编写过多的if、else条件分支语言来控制状态之间的转换。
适配器模式
适配器模式
的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。
假设我们要用googleMap和baiduMap分别以各自的方式在页面中展现地图。
var googleMap = {
show: function () {
console.log('开始渲染谷歌地图');
}
};
var baiduMap = {
display: function () {
console.log('开始渲染百度地图');
}
};
var renderMap = function (map) {
if (map.show instanceof Function) {
map.show();
}
};
复制代码
由于两个地图的展示方法不同,所以不能直接调用renderMap函数,因此我们要为百度地图提供一个适配器。这样我们就可以同时渲染2个地图了。
var baiduMapAdapter = {
show: function () {
return baiduMap.display();
}
};
renderMap(googleMap); // 输出:开始渲染谷歌地图
renderMap(baiduMapAdapter); // 输出:开始渲染百度地图
发布订阅模式
状态变化需要通知其他事件
var salesOffices = {}; // 定义售楼处
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function (fn) { // 增加订阅者
this.clientList.push(fn); // 订阅的消息添加进缓存列表
};
salesOffices.trigger = function () { // 发布消息
for (var i = 0, fn; fn = this.clientList[i++];) {
fn.apply(this, arguments); // (2) // arguments 是发布消息时带上的参数
}
};
salesOffices.listen(function (price, squareMeter) { // 小明订阅消息
console.log('价格= ' + price);
console.log('squareMeter= ' + squareMeter);
});
salesOffices.listen(function (price, squareMeter) { // 小红订阅消息
console.log('价格= ' + price);
console.log('squareMeter= ' + squareMeter);
});
salesOffices.trigger(2000000, 88); // 输出:200 万,88 平方米
salesOffices.trigger(3000000, 110); // 输出:300 万,110 平方米