JavaScript 设计模式综合指南
设计模式是软件工程行业中一个广为人知的概念,因为它为代码复用和可维护性带来了诸多益处。作为一名软件开发人员,您很可能在某个时候偶然接触过这个术语。毫不奇怪,您可能在不知不觉中就已经在开发过程中的某个地方实践过它了。
设计模式用于识别可复用的解决方案,这些解决方案可应用于软件开发人员在软件设计过程中经常遇到的重复问题。它们代表了面向对象软件开发人员长期以来采用的久经考验的解决方案和最佳实践。
本博客将带你了解所有流行的 JavaScript 设计模式。唯一的前提条件是你必须具备 JavaScript 和面向对象编程的基础知识。
我们将从历史角度出发,从面向对象的视角深入探讨各种常见的 JavaScript 设计模式。最终,你将熟悉各种JavaScript 设计模式,并对其实现有一个基本的了解。
让我们开始吧!
设计模式的历史
设计模式的概念自诞生以来就一直存在于编程世界中。但直到1994年,最具影响力的著作之一《设计模式:可复用面向对象软件的要素》出版后,它才正式形成。该书由埃里希·伽马 (Erich Gamma)、理查德·赫尔姆 (Richard Helm)、拉尔夫·约翰逊 (Ralph Johnson ) 和约翰·弗利斯赛德斯 (John Vlissides)撰写,他们后来被称为“四人帮”(GoF)。
本书介绍了23种面向对象的设计模式。自此,“模式方法”在软件工程界开始流行,之后又陆续发现了数十种其他模式。
什么是设计模式?
设计模式可以被认为是用于解决特定设计问题的预制蓝图。它并非可以直接应用于程序的成品代码。相反,它更像是一个模板或描述,可以为您提供解决问题的思路并激发解决方案。因此,针对两种不同的编程场景,实现相同模式的代码可以有所不同。
现在,如果你想知道一个模式是如何被发现的,其实很简单。当同一个解决方案被一遍又一遍地重复时,最终会有人识别它,给它命名,然后详细描述这个解决方案。这就是模式被发现的方式。当然,它们不是一夜之间形成的。
设计模式常常与算法混淆。
设计模式的结构
如上文所述,设计模式的作者会提供文档。尽管模式社区对于文档模板的结构并没有严格的约定,但以下是通常包含的部分。
其他一些部分包括适用性、协作、后果等。
为什么要使用模式?
如前所述,我们每天都在使用模式。它们帮助我们解决反复出现的设计问题。但有必要花时间学习它们吗?让我们来看看设计模式给我们带来的一些关键好处。
1.避免重复造轮子:
大多数常见的设计问题都已经有了与模式相关的明确解决方案。模式是经过验证的解决方案,可以加速开发。
2.代码库维护:
模式有助于实现 DRY(不要重复自己)——这一概念有助于防止代码库变得庞大而笨重。
3.易于重复使用:
重用模式有助于避免在应用程序开发过程中出现可能导致严重问题的细微问题。这还能提高熟悉模式的程序员和架构师的代码可读性。
4.实现高效沟通:
模式丰富了开发人员的词汇量。这使得开发人员能够使用众所周知、易于理解的名称进行软件交互,从而加快沟通速度。
5.提高面向对象的技能:
现在,即使您从未遇到过任何这些问题,学习模式也可以让您深入了解使用面向对象原理解决问题的各种方法。
对模式的批判
随着时间的推移,设计模式也受到了不少批评。让我们来探究一下那些反对模式的流行论点。
1.增加复杂性:
模式使用不当会造成不必要的复杂性。许多新手都遇到过这个问题,他们试图将模式应用到他们能想到的任何地方,即使在一些情况下,简单的代码就可以了。
2.相关性降低:
在《动态语言中的设计模式》一书中,Peter Norvig 指出,1994 年出版的那本由 GoF 撰写的书里,超过一半的设计模式都是为了弥补语言缺失的特性而提出的。很多情况下,模式只是一些临时拼凑的方案,赋予了编程语言当时所缺乏的超能力。
随着语言特性、框架和库的发展,不再需要使用少数模式。
3.懒惰的设计:
正如 Paul Graham 在《书呆子的复仇》(2002)中所说,模式是一种懒惰的设计,指的是开发人员不专注于手头的问题需求。他们可能只是复用现有的设计模式,而不是为问题创建一个新的、合适的设计,因为他们认为这样做就应该这样做。
到目前为止,我们已经了解了什么是设计模式,并讨论了它们的优缺点。现在是时候深入探索各种可用的 JS 设计模式了。
注:在接下来的课程中,我们将探讨经典和现代设计模式的面向对象 JavaScript 实现。需要注意的是,GoF 书中提到的一些经典设计模式随着时间的推移已经不再那么重要。因此,我们将省略它们,并引入来自Addy Osmani 的《学习 JavaScript 设计模式》等资源的现代模式。
JavaScript 设计模式
JavaScript 是当今 Web 开发中最受欢迎的编程语言之一。由于本文将重点介绍 JavaScript 的设计模式,因此我们先快速回顾一下 JavaScript 的基本功能,以帮助大家更顺利地理解它。
a) 编程风格灵活
JavaScript 支持过程式、面向对象和函数式编程风格。
b) 支持一流函数
这意味着函数可以像变量一样作为参数传递给其他函数。
c) 基于原型的继承
虽然 JavaScript 支持对象,但与其他面向对象编程 (OOP) 语言不同,JavaScript 的基本形式中没有类或基于类的继承的概念。相反,它使用一种称为基于原型或基于实例的继承的方法。
注意:在 ES6 中,尽管引入了关键字“class”,但它仍然在本质上利用基于原型的继承。
要了解有关使用 JavaScript 定义“类”的更多信息,请查看 Stoyan Stefanov 的这篇关于定义 JavaScript 类的三种方法的有用文章。
设计模式的分类
根据意图,JavaScript 设计模式可分为 3 个主要类别:
a)创建型设计模式
这些模式专注于处理对象创建机制。程序中基本的对象创建方法可能会增加复杂性。创建型 JS 设计模式旨在通过控制创建过程来解决这个问题。
属于此类别的模式有:构造函数、工厂、原型、单例等。
b)结构设计模式
这些模式关注的是对象组合。它们解释了将对象和类组装成更大结构的简单方法。它们有助于确保当系统的某个部分发生变化时,整个系统结构无需发生改变,从而保持系统的灵活性和高效性。
属于此类别的模式有:模块、装饰器、外观、适配器、代理等。
c)行为设计模式
这些模式专注于改善系统中不同对象之间的沟通和职责分配。
属于此类别的模式很少——责任链、命令、观察者、迭代器、策略、模板等。
基于对分类的理解,让我们来研究一下每种 JavaScript 设计模式。
创建型设计模式
1.构造函数模式
构造函数模式是最简单、最流行、最现代的 JS 设计模式之一。顾名思义,该模式的目的是帮助创建构造函数。
用 Addy 的话来说:
构造函数是一种特殊的方法,用于在分配内存后初始化新创建的对象。在 JavaScript 中,几乎所有东西都是对象,因此我们最感兴趣的是对象构造函数。
例子:
在下面的代码中,我们定义了一个带有属性 name 和 age 的函数/类 Person。getDetails() 方法将以以下格式打印人员的姓名和年龄:
“名字是年龄岁!”
语法有两种格式 - (a)传统的基于函数的语法和(b)EC6 类语法。
然后,我们通过使用 new 关键字调用构造函数方法并传递相应的属性值来实例化 Person 类的对象。
// a) Traditional "function" based syntax
function Person(name,age) {
this.name = name;
this.age = age;
this.getDetails = function () {
console.log(`${this.name} is ${this.age} years old!`);
}
}
// b) ES6 "class" syntax
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
this.getDetails = function () {
console.log(`${this.name} is ${this.age} years old!`);
};
}
}
//Creating new instance of Person
const personOne = new Person('John',20);
personOne.getDetails(); // Output - “John is 20years old!”
2.工厂模式
工厂模式是另一种创建型模式,它关注的是创建对象,但使用某种通用接口。根据 GoF 的书籍,该模式具有以下职责。
“定义一个用于创建对象的接口,但让子类决定实例化哪个类。”
这种模式通常用于需要通过适当的自定义调用来处理具有相似特征但又不同的对象组的情况。举个例子会更清晰。
注意:虽然定义中特别提到需要定义一个接口,但 JavaScript 中并没有接口。因此,我们将使用另一种方法来实现它。
例子:
这里,shapeFactory构造函数负责创建 Rectangle、Square 和 Circle 的新对象。shapeFactory内部的createShape()函数接受参数,并根据参数将对象实例化的责任委托给相应的类。
//Factory method for creating new shape instances
function shapeFactory(){
this.createShape = function (shapeType) {
var shape;
switch(shapeType){
case "rectangle":
shape = new Rectangle();
break;
case "square":
shape = new Square();
break;
case "circle":
shape = new Circle();
break;
default:
shape = new Rectangle();
break;
}
return shape;
}
}
// Constructor for defining new Rectangle
var Rectangle = function () {
this.draw = function () {
console.log('This is a Rectangle');
}
};
// Constructor for defining new Square
var Square = function () {
this.draw = function () {
console.log('This is a Square');
}
};
// Constructor for defining new Circle
var Circle= function () {
this.draw = function () {
console.log('This is a Circle);
}
};
var factory = new shapeFactory();
//Creating instance of factory that makes rectangle,square,circle respectively
var rectangle = factory.createShape('rectangle');
var square = factory.createShape('square');
var circle= factory.createShape('circle');
rectangle.draw();
square.draw();
circle.draw();
/*
OUTPUT
This is a Rectangle
This is a Square
This is a Circle
*/
3.原型模式
支持克隆的对象称为原型。使用原型模式,我们可以通过克隆基于现有对象的模板实例化新对象。
由于原型模式基于原型继承,我们可以利用 JavaScript 原生的原型优势。在之前的 JS 设计模式中,我们试图在 JavaScript 中模仿其他语言的特性,但这里并非如此。
例子:
Object.create
这里我们有一个原型类 car,使用ES5 标准定义的特性进行克隆以创建一个新对象 myCar 。
// Prototype Class
const car = {
noOfWheels: 4,
start() {
return 'started';
},
stop() {
return 'stopped';
},
};
//using Object.create to create clones - as recommended by ES5 standard
const myCar = Object.create(car, { owner: { value: 'John' } });
console.log(myCar.__proto__ === car); // true
4.单例模式
单例模式是一种创建型 JavaScript 设计模式,它将类的实例限制为单个对象。如果不存在该类的实例,则会创建一个新的实例;如果已存在,则仅返回该实例的引用。它也被称为严格模式。
单例模式同时解决了两个问题,违反了单一职责原则。
- 保证一个类只有一个实例。
- 为该实例提供全局访问点。
一个实际的例子是程序不同部分共享一个数据库对象。如果已经存在一个数据库实例,则无需创建新的数据库实例。
该模式的一个缺点是测试难度较大。它包含一些隐藏的依赖对象,很难单独进行测试。
例子:
//Singleton class
var Singleton = (function () {
var instance;
function createDBInstance() {
var object = new Object("I am the DataBase instance");
return object;
}
return {
getDBInstance: function () {
if (!instance) {
instance = createDBInstance();
}
return instance;
}
};
})();
function run() {
var instance1 = Singleton.getDBInstance();
var instance2 = Singleton.getDBInstance();
console.log("Same instance? " + (instance1 === instance2));
}
run(); // OUTPUT = "Same instance? true"
结构设计模式
1.适配器模式
适配器模式是一种结构化的 JS 设计模式,允许接口不兼容的对象或类进行协作。它匹配不同类或对象的接口,使它们即使接口不兼容也能协同工作。它也被称为包装器模式。
现实世界中的类比是尝试将投影仪连接到笔记本电脑。投影仪可能带有 VGA 接口,而笔记本电脑可能带有 HDMI 接口。因此,我们需要一个能够兼容这两个不相关接口的适配器。
该模式将包括一个负责连接不兼容的接口/功能的类。
例子:
以下代码展示了一个在线机票价格计算系统。旧版界面仅以某种方式执行价格计算。新版界面经过改进,新增了用户识别和即时计算等功能。
引入了一个适配器类,通过将旧接口与新接口进行匹配,允许客户端程序继续工作而无需任何 API 更改。
// old interface
function TicketPrice() {
this.request = function(start, end, overweightLuggage) {
// price calculation code...
return "$150.34";
}
}
// new interface
function NewTicketPrice() {
this.login = function(credentials) { /* process credentials */ };
this.setStart = function(start) { /* set start point */ };
this.setDestination = function(destination) { /* set destination */ };
this.calculate = function(overweightLuggage) {
//price calculation code...
return "$120.20";
};
}
// adapter interface
function TicketAdapter(credentials) {
var pricing = new NewTicketPrice();
pricing.login(credentials);
return {
request: function(start, end, overweightLuggage) {
pricing.setStart(start);
pricing.setDestination(end);
return pricing.calculate(overweightLuggage);
}
};
}
var pricing = new TicketPrice();
var credentials = { token: "30a8-6ee1" };
var adapter = new TicketAdapter(credentials);
// original ticket pricing and interface
var price = pricing.request("Bern", "London", 20);
console.log("Old price: " + price);
// new ticket pricing with adapted interface
price = adapter.request("Bern", "London", 20);
console.log("New price: " + price);
2. 复合模式
组合模式是一种结构化的 JavaScript 设计模式,它允许你将对象组合成树形结构,然后像处理单个对象一样处理这些结构。根据 GoF 的书籍,该模式将对象组合成树形结构,以表示部分-整体的层次结构。它也被称为分区 JS 设计模式。
这种模式的完美示例是树形控制。树的节点要么包含单个对象(叶节点),要么包含一组对象(节点子树)。
React 和 Vue 等现代 JS 框架使用组合模式来构建用户界面。整个视图被拆分成多个组件。每个组件可以包含多个组件。与较少的单体对象相比,这种方法更易于开发且可扩展性更高,因此更受青睐。组合模式允许您使用小型对象并将其构建成更大的对象,从而降低了系统的复杂性。
例子:
以下代码解释了文件夹(目录)的结构。此处,目录可以包含两种类型的实体:文件或另一个目录,后者可以包含文件或目录等等。
我们有两个类——文件和目录。我们可以在目录中添加或删除文件,也可以获取文件名,然后显示目录内的所有文件名。
function File(name) {
this.name = name;
}
File.prototype.display = function () {
console.log(this.name);
}
function Directory(name) {
this.name = name;
this.files = [];
}
Directory.prototype.add = function (file) {
this.files.push(file);
}
Directory.prototype.remove = function (file) {
for (let i = 0, length = this.files.length; i < length; i++) {
if (this.files[i] === file) {
this.files.splice(i, 1);
return true;
}
}
return false;
}
Directory.prototype.getFileName = function (index) {
return this.files[index].name;
}
Directory.prototype.display = function() {
console.log(this.name);
for (let i = 0, length = this.files.length; i < length; i++) {
console.log(" ", this.getFileName(i));
}
}
directoryOne = new Directory('Directory One');
directoryTwo = new Directory('Directory Two');
directoryThree = new Directory('Directory Three');
fileOne = new File('File One');
fileTwo = new File('File Two');
fileThree = new File('File Three');
directoryOne.add(fileOne);
directoryOne.add(fileTwo);
directoryTwo.add(fileOne);
directoryThree.add(fileOne);
directoryThree.add(fileTwo);
directoryThree.add(fileThree);
directoryOne.display();
directoryTwo.display();
directoryThree.display();
/*
Directory One
File One
File Two
Directory Two
File One
Directory Three
File One
File Two
File Three
*/
3.模块模式
模块模式是另一种流行的 JavaScript 设计模式,用于保持代码的简洁、分离和有序。模块是一段独立的代码,可以在不影响其他组件的情况下进行更新。由于 JavaScript 不支持访问修饰符的概念,模块有助于模拟私有/公共访问的行为,从而提供封装。
典型的代码结构如下:
(function() {
// declare private variables and/or functions
return {
// declare public variables and/or functions
}
})();
例子:
这里我们可以灵活地重命名,例如我们将 addAnimal 重命名为 add。需要注意的是,我们不能从外部环境调用 removeAnimal,因为它依赖于私有属性容器。
function AnimalContainter () {
//private variables and/or functions
const container = [];
function addAnimal (name) {
container.push(name);
}
function getAllAnimals() {
return container;
}
function removeAnimal(name) {
const index = container.indexOf(name);
if(index < 1) {
throw new Error('Animal not found in container');
}
container.splice(index, 1)
}
return {
public variables and/or functions
add: addAnimal,
get: getAllAnimals,
remove: removeAnimal
}
}
const container = AnimalContainter();
container.add('Hen');
container.add('Goat');
container.add('Sheep');
console.log(container.get()) //Array(3) ["Hen", "Goat", "Sheep"]
container.remove('Sheep')
console.log(container.get()); //Array(2) ["Hen", "Goat"]
4.装饰器模式
装饰器是一种结构化的 JS 设计模式,旨在促进代码复用。这种模式允许将行为动态地添加到单个对象,而不会影响同一类中其他对象的行为。装饰器还可以提供一种灵活的替代子类化方法来扩展功能。
由于 JavaScript 允许我们动态地向对象添加方法和属性,因此实现此 JavaScript 模式的过程非常简单。请查看 Addy Osmani 的文章,了解更多关于装饰器的信息。
例子:
我们先来看一个简单的实现。
// A vehicle constructor
function Vehicle( vehicleType ){
// some sane defaults
this.vehicleType = vehicleType || "car";
this.model = "default";
this.license = "00000-000";
}
// Test instance for a basic vehicle
var testInstance = new Vehicle( "car" );
console.log( testInstance );
// Outputs:
// vehicle: car, model:default, license: 00000-000
// Lets create a new instance of vehicle, to be decorated
var truck = new Vehicle( "truck" );
// New functionality we're decorating vehicle with
truck.setModel = function( modelName ){
this.model = modelName;
};
truck.setColor = function( color ){
this.color = color;
};
// Test the value setters and value assignment works correctly
truck.setModel( "CAT" );
truck.setColor( "blue" );
console.log( truck );
// Outputs:
// vehicle:truck, model:CAT, color: blue
// Demonstrate "vehicle" is still unaltered
var secondInstance = new Vehicle( "car" );
console.log( secondInstance );
// Outputs:
// vehicle: car, model:default, license: 00000-000
5. 外观模式
外观模式由外观(Facade)组成,外观是一个对象,充当结构更为复杂的代码的“正面”。当系统非常复杂或难以理解时,开发人员通常会使用此模式,以便为客户端提供更简单的界面。这有助于在公开显示的内容和幕后实现的内容之间创建一个抽象层。
例子:
这里的抵押贷款是银行、信贷和背景的幌子。
var Mortgage = function(name) {
this.name = name;
}
Mortgage.prototype = {
applyFor: function(amount) {
// access multiple subsystems...
var result = "approved";
if (!new Bank().verify(this.name, amount)) {
result = "denied";
} else if (!new Credit().get(this.name)) {
result = "denied";
} else if (!new Background().check(this.name)) {
result = "denied";
}
return this.name + " has been " + result +
" for a " + amount + " mortgage";
}
}
var Bank = function() {
this.verify = function(name, amount) {
// complex logic ...
return true;
}
}
var Credit = function() {
this.get = function(name) {
// complex logic ...
return true;
}
}
var Background = function() {
this.check = function(name) {
// complex logic ...
return true;
}
}
function run() {
var mortgage = new Mortgage("Joan Templeton");
var result = mortgage.applyFor("$100,000");
alert(result);
}
6.代理模式
顾名思义,代理模式为另一个对象提供替代物或占位符,以控制访问、降低成本并降低复杂性。代理可以与任何对象交互——网络连接、内存中的大型对象、文件或其他一些昂贵或无法复制的资源。
在这里,我们将创建一个代理对象来“替代”原始对象。代理接口将与原始对象的接口相同,以便客户端甚至可能不知道他们正在处理代理而不是真实对象。代理可以提供额外的功能,例如缓存、检查某些先决条件等。
代理模式适用的常见情况有三种。
- 虚拟代理是创建成本高昂或资源密集型对象的占位符。
- 远程代理控制对远程对象的访问。
- 保护性代理控制对敏感主对象的访问权限。在转发请求之前,会检查调用者的访问权限。
例子:
以下代码将帮助您了解代理实现的要点。我们有一个外部 API FlightListAPI 用于访问航班详情数据库。我们将创建一个代理 FlightListProxy,它将作为客户端访问该 API 的接口。
/* External API*/
var FlightListAPI = function() {
//creation
};
FlightListAPI.prototype = {
getFlight: function() {
// get master list of flights
console.log('Generating flight List');
},
searchFlight: function(flightDetails) {
// search through the flight list based on criteria
console.log('Searching for flight');
},
addFlight: function(flightData) {
// add a new flight to the database
console.log('Adding new flight to DB');
}
};
// creating the proxy
var FlightListProxy = function() {
// getting a reference to the original object
this.flightList = new FlightListAPI();
};
FlightListProxy.prototype = {
getFlight: function() {
return this.flightList.getFlight();
},
searchFlight: function(flightDetails) {
return this.flightList.searchFlight(flightDetails);
},
addFlight: function(flightData) {
return this.flightList.addFlight(flightData);
},
};
console.log("----------With Proxy----------")
const proxy = new FlightListProxy()
console.log(proxy.getFlight());
/*
OUTPUT
----------With Proxy----------
Generating flight List
*/
行为设计模式
1. 责任链模式
这是一种行为型 JavaScript 设计模式,它为一个请求创建一系列接收者对象。这种模式提倡松耦合。我们可以避免请求发送者和接收者之间的耦合,并且多个接收者可以处理同一个请求。
接收对象将被链接在一起,它们可以选择对请求采取行动或将其传递给下一个接收对象。也可以轻松地将新的接收对象添加到链中。
DOM 中的事件处理是责任链模式的一种实现。
一旦触发事件,它就会在 DOM 层次结构中传播,调用遇到的每个事件处理程序,直到找到适当的“事件监听器”并对其采取行动。
例子:
让我们考虑一下 ATM 机的场景。当我们请求取款金额时,机器会处理请求,并以可用面额($100、$50、$20、$10、$5、$1)的组合形式取出金额。
在这段请求金额的代码中,会创建一个 Request 对象。然后,该对象会调用一系列 get 调用,这些调用串联在一起,每个调用处理一个特定的面额。最终,用户会收到满足金额要求的纸币组合作为金额。
var Request = function(amount) {
this.amount = amount;
console.log("Request Amount:" +this.amount);
}
Request.prototype = {
get: function(bill) {
var count = Math.floor(this.amount / bill);
this.amount -= count * bill;
console.log("Dispense " + count + " $" + bill + " bills");
return this;
}
}
function run() {
var request = new Request(378); //Requesting amount
request.get(100).get(50).get(20).get(10).get(5).get(1);
}
2. 命令模式
命令模式是一种行为型 JS 设计模式,旨在将动作或操作封装为对象。当我们希望将执行命令的对象与发出命令的对象解耦或分离时,此模式非常有用。命令对象允许您集中处理这些动作/操作。
命令模式涉及的四个参与者是命令、接收者、调用者和客户端。
- 命令– 命令对象了解接收器并调用接收器的方法。接收器方法的参数值存储在命令中。
- 客户端——客户端的职责是创建命令对象并将其传递给调用者。
- 调用者——调用者从客户端接收命令对象,其唯一职责是调用(或调用)命令。
- 接收者——然后,接收者接收命令并根据接收到的命令寻找要调用的方法。
例子:
在我们的示例中,计算器对象包含四个方法——加、减、除和乘。命令对象定义了一个方法execute,它负责调用方法。
var calculator = {
add: function(x, y) {
return x + y;
},
subtract: function(x, y) {
return x - y;
},
divide: function(x,y){
return x/y;
},
multiply: function (x,y){
return x*y;
}
}
var manager = {
execute: function(name, args) {
if (name in calculator) {
return calculator[name].apply(calculator, [].slice.call(arguments, 1));
}
return false;
}
}
console.log(manager.execute("add", 5, 2)); // prints 7
console.log(manager.execute("multiply", 2, 4)); // prints 8
3.观察者模式
观察者模式是一种行为型 JS 设计模式,它允许你定义一种订阅机制,以便将发生在它们所观察的对象(主体)上的任何事件通知给多个对象(观察者)。这种模式也称为发布/订阅 (Publishing/Subscription),即发布/订阅 (Publishing/Subscription)。它定义了对象之间的一对多依赖关系,促进了松耦合,并有助于良好的面向对象设计。
观察者模式是事件驱动编程的基础。我们编写事件处理函数,当某个事件触发时,它会收到通知。
例子:
我们创建了一个 Subject 函数 Click,并使用原型对其进行了扩展。我们还创建了用于订阅和取消订阅 Observer 集合的方法,这些方法由 clickHandler 函数处理。此外,还有一个 fire 方法,用于将 Subject 类对象中的任何更改传播给已订阅的 Observers。
function Click() {
this.observers = []; // observers
}
Click.prototype = {
subscribe: function(fn) {
this.observers.push(fn);
},
unsubscribe: function(fn) {
this.observers = this.observers.filter(
function(item) {
if (item !== fn) {
return item;
}
}
);
},
fire: function(o, thisObj) {
var scope = thisObj;
this.observers.forEach(function(item) {
item.call(scope, o);
});
}
}
function run() {
var clickHandler = function(item) {
console.log("Fired:" +item);
};
var click = new Click();
click.subscribe(clickHandler);
click.fire('event #1');
click.unsubscribe(clickHandler);
click.fire('event #2');
click.subscribe(clickHandler);
click.fire('event #3');
}
/* OUTPUT:
Fired:event #1
Fired:event #3
*/
4.迭代器模式
迭代器模式允许您顺序访问和遍历聚合对象(集合)的元素,而无需暴露其底层表示。此模式允许 JavaScript 开发人员设计更加灵活和复杂的循环结构。ES6 中引入了迭代器和生成器,这进一步辅助了迭代模式的实现。
例子:
这是一段简单直接的从前向后迭代代码。我们为迭代器定义了两个方法——hasNext() 和 next()。
const items = [1,"hello",false,99.8];
function Iterator(items){
this.items = items;
this.index = 0; // to start from beginning position of array
}
Iterator.prototype = {
// returns true if a next element is available
hasNext: function(){
return this.index < this.items.length;
},
//returns next element
next: function(){
return this.items[this.index++]
}
}
//Instantiate object for Iterator
const iterator = new Iterator(items);
while(iterator.hasNext()){
console.log(iterator.next());
}
/*
OUTPUT
1
hello
false
99.8
*/
5.模板模式
模板模式通过一些高级步骤定义了算法运行的框架。这些步骤本身由与模板方法位于同一类中的附加辅助方法实现。实现这些步骤的对象保留了算法的原始结构,但可以选择重新定义或调整某些步骤。
例子:
这里我们有一个抽象类 datastore,它提供了一个接口,可以通过定义算法的基本步骤来实现模板方法。此外,我们还有一个具体的 MySQL 类,它实现了抽象类中定义的基本步骤。
// implement template method
var datastore = {
process: function() {
this.connect();
this.select();
this.disconnect();
return true;
}
};
function inherit(proto) {
var F = function() { };
F.prototype = proto;
return new F();
}
function run() {
var mySql = inherit(datastore);
// implement template steps
mySql.connect = function() {
console.log("MySQL: connect step");
};
mySql.select = function() {
console.log("MySQL: select step");
};
mySql.disconnect = function() {
console.log("MySQL: disconnect step");
};
mySql.process();
}
run();
/*
MySQL: connect step
MySQL: select step
MySQL: disconnect step
*/
6.策略模式
策略模式允许在运行时动态地选择一组算法中的一种。该模式定义了一组算法,封装了每个算法,并使它们在运行时可以互换,而无需客户端干预。
例子:
我们创建了一个类 Shipping,它封装了所有可能的包裹运送策略——FedEx、UPS 和 USPS。使用此模式,我们可以在运行时切换策略并生成相应的输出。
//Strategy1
function FedEx(){
this.calculate = package => {
//calculations happen here..
return 2.99
}
}
//Strategy2
function UPS(){
this.calculate = package => {
//calculations happen here..
return 1.59
}
}
//Strategy3
function USPS(){
this.calculate = package => {
//calculations happen here..
return 4.5
}
}
// encapsulation
function Shipping(){
this.company = "";
this.setStrategy = (company) => {
this.company=company;
}
this.calculate = (package) =>{
return this.company.calculate(package);
}
}
//usage
const fedex = new FedEx();
const ups = new UPS();
const usps = new USPS();
const package = { from: 'Alabama',to:'Georgia',weight:1.5};
const shipping = new Shipping();
shipping.setStrategy(fedex);
console.log("Fedex:" +shipping.calculate(package)); // OUTPUT => "Fedex:2.99"
反模式
了解设计模式固然重要,但了解反模式也同样重要。如果说设计模式是最佳实践,那么反模式则恰恰相反。
“反模式”一词由安德鲁·科尼格于 1995 年提出。根据科尼格的说法,反模式是指针对特定问题,导致糟糕局面的糟糕解决方案。
JavaScript 中反模式的几个例子如下:
- 通过在全局上下文中定义大量变量来污染全局命名空间
- 将字符串而不是函数传递给 setTimeout 或 setInterval,因为这会在内部触发 eval() 的使用。
- 修改 Object 类原型(这是一个特别糟糕的反模式)
总而言之,反模式是一种值得记录的糟糕设计。了解反模式可以帮助你识别代码中的此类反模式,从而提高代码质量。
应用设计模式和测试
一旦设计模式得以实现并验证,我们需要确保它能够在多个浏览器和浏览器版本之间无缝运行。LambdaTest 是一个跨浏览器测试平台,可用于手动和自动跨浏览器测试。它涵盖了 2000 多个真实浏览器和浏览器版本,并允许在所有主流浏览器和浏览器版本中进行浏览器兼容性回归测试。
您还可以利用LT 浏览器(一种开发人员友好型工具)来对您的设计模式在流行设备和视口中的响应能力进行详细分析。
结论
设计模式代表了经验丰富的面向对象软件开发人员所采用的一些最佳实践。它们是针对各种软件设计问题久经考验的解决方案。在本文中,我们探讨了 JavaScript 中的常见设计模式。此外,我们还简要讨论了反模式以及如何在 LambdaTest 平台上使用这些模式测试网站。希望本章能够帮助您熟悉各种 JavaScript 设计模式。想要深入了解这些概念,请参阅Addy Osmani 撰写的《学习 JavaScript 设计模式》 。
文章来源:https://dev.to/jainrahul/a-compressive-guide-to-javascript-design-patterns-2h7d