JS 设计模式快速参考指南
在网络开发这个不断革新的领域里,JavaScript始终是一个基础性的重要组成部分。随着这种灵活的脚本语言继续决定数字世界的面貌,任何希望开发出稳定且易于维护的应用程序的开发者都必须对其纷繁复杂的性质有深入了解。本文将带您进入JavaScript设计模式的世界,这是一个充满魅力且必不可少的工具集,它能让开发者构架出可扩展、高效、并能有效防错的代码。我们将一起展开这段探秘之旅,解锁JavaScript设计模式的神秘面纱,一探其中的重要性、如何落实到实际,以及它们在现实世界的应用。不管您是一个有着丰富经验、期望增强知识深度的资深开发者,还是一个急于掌握基础的新入门者,让我们开始深入研究,体验在JavaScript世界里带着目的进行设计的艺术吧。
创建型模式
创建型模式属于设计模式的一种类别,它们专门用于解决与对象创建过程中出现的常见问题。
单例模式
单例模式确保了某个特定类的对象只能有一个实例。这种模式减少了对全局变量的依赖,从而降低了名字冲突的可能性。
举个例子,当我们在 constructor()
构造函数中检查的时候,会判断已有的 Animal
类实例是否存在。如果不存在,那么我们就需要创建一个新的实例。
class Animal {
constructor() {
if (typeof Animal.instance === 'object') {
return Animal.instance;
}
Animal.instance = this;
return this;
}
}
export default Animal;
当你只需要一个类的实例时使用这个模式。
原型模式
原型模式基于具有默认属性值的现有对象创建新对象。
以这个例子为例,我们可以利用 clone()
方法复制一个具有相同名称和重量的 Fruit
对象,这样就可以生成一个新的实例,它保留了原有实例的特性。
class Fruit {
constructor(name, weight) {
this.name = name;
this.weight = weight;
}
clone() {
return new Fruit(this.name, this.weight);
}
}
export default Fruit;
在需要动态地在程序运行过程中创建类的实例的情况下,推荐使用原型模式。
工厂模式
工厂模式通过委托子类来决定实例化哪一个类。
以这个例子为例,MovieFactory
将根据特定的情况决定需要创建哪种类型的电影对象。
class MovieFactory {
create(genre) {
if (genre === 'Adventure') return new Movie(genre, 10000);
if (genre === 'Action') return new Movie(genre, 11000);
}
}
class Movie {
constructor(type, price) {
this.type = type;
this.price = price;
}
}
export default MovieFactory;
当你希望由一个子类来决定要创建什么对象时,使用这个模式。
抽象工厂模式
抽象工厂模式根据主题来创建新对象,而无需直接指定这些对象的具体类别。
function foodProducer(kind) {
if (kind === 'protein') return proteinPattern;
if (kind === 'fat') return fatPattern;
return carbohydratesPattern;
}
function proteinPattern() {
return new Protein();
}
function fatPattern() {
return new Fat();
}
function carbohydratesPattern() {
return new Carbohydrates();
}
class Protein {
info() {
return 'I am Protein.';
}
}
class Fat {
info() {
return 'I am Fat.';
}
}
class Carbohydrates {
info() {
return 'I am carbohydrates.';
}
}
export default foodProducer;
当系统的设计不受其所产出物品的结构或形式影响时,应当采用这种模式。
结构型模式
结构型模式属于设计模式的一个种类,主要解决对象和类组合场景中的典型问题。
适配器模式
适配器模式允许我们创建类接口转换为另一个,使得本不兼容的类可以协同工作。
以这个例子为例,我们使用了 SoldierAdapter
来设配在当前系统中的遗留方法 attack()
,从而支持 SuperSoldiers
的版本。
class Soldier {
constructor(level) {
this.level = level;
}
attack() {
return this.level * 1;
}
}
class SuperSoldier {
constructor(level) {
this.level = level;
}
attackWithShield() {
return this.level * 10;
}
}
class SoldierAdapter {
constructor(superSoldier) {
this.superSoldier = superSoldier;
}
attack() {
return this.superSoldier.attackWithShield();
}
}
export { Soldier, SuperSoldier, SoldierAdapter };
当你需要使用现有的类但它们的接口不兼容时,可以使用这种模式。
桥接模式
桥接模式允许我们在类中设计一个接口,以便根据不同的输入和输出实例需求,构建不同实现方式。
例如,我们可以在士兵种类和武器类型之间搭建桥接,确保武器实例能够正确地配合不同的士兵使用。
class Soldier {
constructor(weapon) {
this.weapon = weapon;
}
}
class SuperSoldier extends Soldier {
constructor(weapon) {
super(weapon);
}
attack() {
return 'SuperSoldier, Weapon: ' + this.weapon.get();
}
}
class IronMan extends Soldier {
constructor(weapon) {
super(weapon);
}
attack() {
return 'Ironman, Weapon: ' + this.ink.get();
}
}
class Weapon {
constructor(type) {
this.type = type;
}
get() {
return this.type;
}
}
class Shield extends Weapon {
constructor() {
super('shield');
}
}
class Rocket extends Weapon {
constructor() {
super('rocket');
}
}
export { SuperSoldier, IronMan, Shield, Rocket };
当你需要在运行时从一个抽象中使用一个特定的实现时,可以使用这种模式。
组合模式
组合模式允许创造出具有属性的对象,这些属性基本项或对象的集合。集合中的每个项本身可以包含其他集合,从而创建深层嵌套的结构。。
例如,在该模式下,我们构建一个计算设备的子系统,并将其组织在一个 Cabinet
中。这个系统中的每一个构件可以是不同的实例。
//Equipment
class Equipment {
getPrice() {
return this.price || 0;
}
getName() {
return this.name;
}
setName(name) {
this.name = name;
}
}
class Pattern extends Equipment {
constructor() {
super();
this.equipments = [];
}
add(equipment) {
this.equipments.push(equipment);
}
getPrice() {
return this.equipments
.map(equipment => {
return equipment.getPrice();
})
.reduce((a, b) => {
return a + b;
});
}
}
class Cabbinet extends Pattern {
constructor() {
super();
this.setName('cabbinet');
}
}
// --- leafs ---
class FloppyDisk extends Equipment {
constructor() {
super();
this.setName('Floppy Disk');
this.price = 70;
}
}
class HardDrive extends Equipment {
constructor() {
super();
this.setName('Hard Drive');
this.price = 250;
}
}
class Memory extends Equipment {
constructor() {
super();
this.setName('Memory');
this.price = 280;
}
}
export { Cabbinet, FloppyDisk, HardDrive, Memory };
当你想要表示对象的层次结构时,可以使用这种模式。
装饰者模式
装饰者模式允许在运行时动态地扩展对象的行为。
在这个例子中,我们使用装饰者来扩展 Facebook 通知中的行为。
class Notification {
constructor(kind) {
this.kind = kind || "Generic";
}
getInfo() {
return `I'm a ${this.kind} Notification`;
}
}
class FacebookNotification extends Notification {
constructor() {
super("Facebook");
}
setNotification(msg) {
this.message = msg;
}
getInfo() {
return `${super.getInfo()} with the message: ${this.message}`;
}
}
class SMSNotification extends Notification {
constructor() {
super("SMS");
}
getInfo() {
return super.getInfo();
}
}
export { FacebookNotification, SMSNotification };
在不影响其他对象的情况下,如果你想在运行时为一个对象添加扩展,可以使用这种模式。
外观模式
外观模式为子系统中的多个接口提供了一个更加简洁的界面。该模式定义了一个上层接口,让子系统变得更加容易使用。
以这个示例为例,我们设计了一个名为 Cart
的简洁接口,它将多个子系统当中的复杂操作,比如 Discount
、Shipping
和 Fees
,统统封装起来,简化了使用流程。
class Cart {
constructor() {
this.discount = new Discount();
this.shipping = new Shipping();
this.fees = new Fees();
}
calc(price) {
price = this.discount.calc(price);
price = this.fees.calc(price);
price += this.shipping.calc();
return price;
}
}
class Discount {
calc(value) {
return value * 0.85;
}
}
class Shipping {
calc() {
return 500;
}
}
class Fees {
calc(value) {
return value * 1.1;
}
}
export default Cart;
当你想简化对复杂子系统的操作,提供一个易于简单使用的接口时,可以使用这种模式。
享元模式
享元模式可以通过共享众多细粒度对象来有效节省内存。这些共享的享元对象是不可变的,意味着它们无法被修改,因为它们体现了可与其他对象共享的特点。
在这个例子中,我们正在管理和创建一些配方或烹饪应用的原料。
class Ingredient {
constructor(name) {
this.name = name;
}
getInfo() {
return `I'm a ${this.name}`
}
}
class Ingredients {
constructor() {
this.ingredients = {};
}
create(name) {
let ingredient = this.ingredients[name];
if (ingredient) return ingredient;
this.ingredients[name] = new Ingredient(name);
return this.ingredients[name];
}
}
export { Ingredients };
在应用程序需要处理大量小对象,它们的存储成本很高且这些对象的具体身份并不关键时,应该考虑使用享元模式。
代理模式
代理模式通过引入一个代理或占位符对象来代表另一个对象,从而控制外部对该对象的访问权限。
在这个应用案例中,我们利用代理模式来确保飞行员的年龄符合规定的限制。
class Plane {
fly() {
return 'flying';
}
}
class PilotProxy {
constructor(pilot) {
this.pilot = pilot;
}
fly() {
return this.pilot.age < 18 ? `too young to fly` : new Plane().fly();
}
}
class Pilot {
constructor(age) {
this.age = age;
}
}
export { Plane, PilotProxy, Pilot };
当一个对象受到严格限制,无法执行其应有的功能时,使用这种模式。
行为型模式
行为模式属于设计模式的分类之一,用于解决对象间通信及职责分配过程中遇到的常见问题。
职责链模式
职责链模式允许沿一个对象链传递请求,,链中的每个对象都有机会处理该请求。当收到请求时,链上的每个处理程序需要决定自己来处理请求或是将请求传递给链中的下一个处程序。
在此示例中,我们正在将 Discount
类串联起来,以处理购物车中折扣额度的请求
class ShoppingCart {
constructor() {
this.products = [];
}
addProduct(p) {
this.products.push(p);
}
}
class Discount {
calc(products) {
let ndiscount = new NumberDiscount();
let pdiscount = new PriceDiscount();
let none = new NoneDiscount();
ndiscount.setNext(pdiscount);
pdiscount.setNext(none);
return ndiscount.exec(products);
}
}
class NumberDiscount {
constructor() {
this.next = null;
}
setNext(fn) {
this.next = fn;
}
exec(products) {
let result = 0;
if (products.length > 3) result = 0.05;
return result + this.next.exec(products);
}
}
class PriceDiscount {
constructor() {
this.next = null;
}
setNext(fn) {
this.next = fn;
}
exec(products) {
let result = 0;
let total = products.reduce((a, b) => (a + b), 0);
if (total >= 500) result = 0.1;
return result + this.next.exec(products);
}
}
class NoneDiscount {
exec() {
return 0;
}
}
export { ShoppingCart, Discount };
当一个请求可能由多个对象处理,而具体由哪个对象处理将在程序运行时确定时,应当使用这种模式。
命令模式
命令模式允许一个请求封装在一个对象中。这样的转换让你可以把请求当做方法的参数进行传递,还能够延迟或排队执行请求,并且支持可撤销的操作。
在这个示例中,我们把 on/off
操作指令封装成对象,并把这些对象作为参数传入 Car 类的构造器中。
class Car {
constructor(instruction) {
this.instruction = instruction;
}
execute() {
this.instruction.execute();
}
}
class Engine {
constructor() {
this.state = false;
}
on() {
this.state = true;
}
off() {
this.state = false;
}
}
class OnInstruction {
constructor(engine) {
this.engine = engine;
}
execute() {
this.engine.on();
}
}
class OffInstruction {
constructor(engine) {
this.engine = engine;
}
execute() {
this.engine.off();
}
}
export { Car, Engine, OnInstruction, OffInstruction };
当你有一个请求队列需要处理,或者你想要有一个撤销动作时,使用这种模式。
解析器模式
解释器模式允许我们为简单语言定义一套语法规则,适用于那些经常出现的问题;我们可以把问题转化为简单语言的句子,然后让解释器通过解释这些句子来解决问题。
在这个例子中,我们创建了一个简单的数来进行乘法和数的幂次方。
class Mul {
constructor(left, right) {
this.left = left;
this.right = right;
}
interpreter() {
return this.left.interpreter() * this.right.interpreter();
}
}
class Pow {
constructor(left, right) {
this.left = left;
this.right = right;
}
interpreter() {
return this.left.interpreter() - this.right.interpreter();
}
}
class Num {
constructor(val) {
this.val = val;
}
interpreter() {
return this.val;
}
}
export { Num, Mul, Pow };
当你想要解释给定的语言,并且你可以将语句表示为抽象语法树时,使用这种模式。
迭代器模式
迭代器模式允许访问集合中的元素,同时不必暴露集合的内部结构。
在这个示例中,我们将会创建一个简单的迭代器,它将接收一个元素数组,并且我们可以使用 next()
和 hasNext()
这两种方法来遍历所有元素。
class Iterator {
constructor(el) {
this.index = 0;
this.elements = el;
}
next() {
return this.elements[this.index++];
}
hasNext() {
return this.index < this.elements.length;
}
}
export default Iterator;
当你希望访问某个对象包含的数据集合,且无需了解该对象内部是如何构建的,此时应当使用这种模式。
中介者模式
中介者模式通过定义一个封装了一组对象交互方式的对象,从而减少这些对象间复杂的依赖关系。
在这个示例中,我们将创建一个名为 TrafficTower
的中介者类,它能够使我们能够知晓所有 Airplane
实例的具体位置信息。
class TrafficTower {
constructor() {
this.airplanes = [];
}
getPositions() {
return this.airplanes.map(airplane => {
return airplane.position.showPosition();
});
}
}
class Airplane {
constructor(position, trafficTower) {
this.position = position;
this.trafficTower = trafficTower;
this.trafficTower.airplanes.push(this);
}
getPositions() {
return this.trafficTower.getPositions();
}
}
class Position {
constructor(x,y) {
this.x = x;
this.y = y;
}
showPosition() {
return `My Position is ${x} and ${y}`;
}
}
export { TrafficTower, Airplane, Position };
在一组对象需要相互通信,且通信方式存在复杂性时,应当采用这种模式。
备忘录模式
备忘录模式允许记录并保存对象的当前内部状态,以便该对象可以稍后恢复到这一状态。
在这个例子中,我们正在创建一个简单方法来存储变量值,并在需要时恢复特定快照。
class Memento {
constructor(value) {
this.value = value;
}
}
const originator = {
store: function(val) {
return new Memento(val);
},
restore: function(memento) {
return memento.value;
}
};
class Keeper {
constructor() {
this.values = [];
}
addMemento(memento) {
this.values.push(memento);
}
getMemento(index) {
return this.values[index];
}
}
export { originator, Keeper };
当你想制作对象状态的快照以便能够恢复对象的之前状态时,使用这种模式。
观察者模式
观察者模式允许定义对象之间的一对多依赖关系,以便当一个对象状态发生变化时,所有依赖于它的对象都将被自动通知和更新。
在这个例子中,我们正在创建一个简单的产品类,其他类可以通过 register()
方法注册来监听该产品类变化信息,一旦产品类有所更新,notifyAll()
方法将负责通知所有的观察者这些变化。
class ObservedProduct {
constructor() {
this.price = 0;
this.actions = [];
}
setBasePrice(val) {
this.price = val;
this.notifyAll();
}
register(observer) {
this.actions.push(observer);
}
unregister(observer) {
this.actions.remove.filter(function(el) {
return el !== observer;
});
}
notifyAll() {
return this.actions.forEach(
function(el) {
el.update(this);
}.bind(this)
);
}
}
class fees {
update(product) {
product.price = product.price * 1.2;
}
}
class profit {
update(product) {
product.price = product.price * 2;
}
}
export { ObservedProduct, fees, profit };
当一个对象的状态发生变化可能需要改变其他对象,并且这些对象集合无法预知或动态更改时,使用这种模式。
状态模式
状态模式允许一个对象在其内部状态变化时改变其行为。
在这个例子里,我们将在 Order
类中实现一个简单的状态模式,通过 next()
方法来更新订单的当前状态。
class OrderStatus {
constructor(name, nextStatus) {
this.name = name;
this.nextStatus = nextStatus;
}
next() {
return new this.nextStatus();
}
}
class WaitingForPayment extends OrderStatus {
constructor() {
super('waitingForPayment', Shipping);
}
}
class Shipping extends OrderStatus {
constructor() {
super('shipping', Delivered);
}
}
class Delivered extends OrderStatus {
constructor() {
super('delivered', Delivered);
}
}
class Order {
constructor() {
this.state = new WaitingForPayment();
}
nextPattern() {
this.state = this.state.next();
}
}
export default Order;
在对象的行为基于当前状态,并且在运行期间其行为会随着状态变化而变化的情况下,状态模式就非常适用。
策略模式
策略模式允许定义一组算法,独立封装每种算法,并保证它们之间可以互相替换。
在本例中,我们设计了多种折扣策略,这些策略可以在购物车中使用。关键之处在于我们可以在构造器中传入不同的折扣函数,从而灵活地更换折扣计算方式,通过这种方式改变折扣金额。
class ShoppingCart {
constructor(discount) {
this.discount = discount;
this.amount = 0;
}
checkout() {
return this.discount(this.amount);
}
setAmount(amount) {
this.amount = amount;
}
}
function guest(amount) {
return amount;
}
function regular(amount) {
return amount * 0.9;
}
function premium(amount) {
return amount * 0.8;
}
export { ShoppingCart, guest, regular, premium };
当存在许多类似的类,它们的区别仅在于执行某些行为的方式时,可以采用这种模式。
模板方法模式
模板模式允许在基类中定义算法的框架,同时允许子类能够重新算法中的某些特定步骤,却不会改变算法的整体结构。
在这个例子中,我们设计了一个基础模板方法来计算税收,并在 VAT
和 GST
中拓展这一模板。通过这种方式,我们能在不同的税种类里复用同一算法框架。
class Tax {
calc(value) {
if (value >= 1000) value = this.overThousand(value);
return this.complementaryFee(value);
}
complementaryFee(value) {
return value + 10;
}
}
class VAT extends Tax {
constructor() {
super();
}
overThousand(value) {
return value * 1.1;
}
}
class GST extends Tax {
constructor() {
super();
}
overThousand(value) {
return value * 1.2;
}
}
export { VAT, GST };
当你仅仅希望允许用户定制算法中的某些步骤,而不是改变算法的整体或结构时,可以采用这个模板模式。
访问者模式
访问者模式允许将算法从它们操作的对象上分离出来。
在这个例子中,我们创建了一个结构来为两种类型的员工计算奖金,这样我们可以将奖金方法扩展到越来越多的员工类型,比如 CEO 奖金、VP 奖金等等。
function bonusPattern(employee) {
if (employee instanceof Manager) employee.bonus = employee.salary * 2;
if (employee instanceof Developer) employee.bonus = employee.salary;
}
class Employee {
constructor(salary) {
this.bonus = 0;
this.salary = salary;
}
accept(item) {
item(this);
}
}
class Manager extends Employee {
constructor(salary) {
super(salary);
}
}
class Developer extends Employee {
constructor(salary) {
super(salary);
}
}
export { Developer, Manager, bonusPattern };
当你需要处理的对象结构中包含大量类,并且你希望对该结构中依赖于其类的元素执行操作时,使用这种模式。