该材料的作者(我们发表其翻译)说,开始一个项目时,他们不会立即开始编写代码。 首先,他们确定项目的目的和范围,然后-确定项目应有的机会。 之后,他们要么立即编写代码,要么,如果这是一个相当复杂的项目,则他们选择合适的设计模式作为其基础。 本材料是关于JavaScript设计模式的。 它主要是为初学者设计的。

什么是设计模式?
在软件开发领域,设计模式是可重复的体系结构设计,是在某些频繁发生的上下文中解决设计问题的方法。 设计模式是对专业软件开发人员经验的总结。 设计模式可以视为根据编写程序的一种模式。
为什么需要设计模式?
许多程序员要么认为设计模式是浪费时间,要么就是根本不知道如何正确应用它们。 但是,使用合适的模式可以帮助编写更好和更易理解的代码,由于其可理解性,因此更易于维护。
也许最重要的是,模式的使用为软件开发人员提供了一些类似于知名术语词典的功能,例如在解析别人的代码时非常有用。 模式为那些试图处理项目设备的人揭示了程序某些片段的目的。
例如,如果您使用“装饰器”模式,它将立即通知来项目的新程序员关于特定代码段可以解决哪些任务以及为什么需要它的原因。 因此,这样的程序员将能够花更多的时间在程序解决的实际任务上,而不是试图了解其内部结构。
现在,我们已经弄清楚了什么是设计模式以及它们的用途,我们将继续介绍这些模式本身,并描述使用JavaScript的实现。
模式“模块”
模块是一段独立的代码,可以更改而不会影响其他项目代码。 另外,由于模块为它们中声明的变量创建了单独的可见性区域,因此模块可以避免出现可见性区域污染的现象。 如果为一个项目编写的模块的机制是通用的,并且与特定项目的细节无关,则可以在其他项目中重用。
模块是任何现代JavaScript应用程序不可或缺的一部分。 它们有助于保持代码的清洁度,帮助将代码分成有意义的片段,并帮助进行组织。 JavaScript有多种创建模块的方法,其中一种就是“模块”模式。
与其他编程语言不同,JavaScript没有访问修饰符。 也就是说,变量不能声明为私有或公共。 结果,“模块”模式也被用来模仿封装的概念。
此模式使用IIFE(立即调用的函数表达式,函数的闭包和范围)来模仿此概念。 例如:
const myModule = (function() { const privateVariable = 'Hello World'; function privateMethod() { console.log(privateVariable); } return { publicMethod: function() { privateMethod(); } } })(); myModule.publicMethod();
由于我们具有IIFE,因此代码将立即执行,并将表达式返回的对象分配给常量
myModule
。 由于存在关闭的事实,即使在IIFE完成其工作之后,返回的对象也可以访问在IIFE中声明的函数和变量。
结果,IIFE内部声明的变量和函数对它们外部可见性范围内的机制隐藏了。 它们原来是
myModule
常数的私有实体。
执行此代码后,
myModule
将如下所示:
const myModule = { publicMethod: function() { privateMethod(); }};
也就是说,引用此常量,您可以调用
publicMethod()
对象的public方法,该对象又将调用私有方法
privateMethod()
。 例如:
// 'Hello World' module.publicMethod();
开放模块模式
显示模块模式是Christian Heilmann提出的模块模式的略微改进版本。 “模块”模式的问题在于,我们必须创建公共函数才能访问私有函数和变量。
在这种模式下,我们将私有函数分配给要公开的返回对象的属性。 这就是为什么将该模式称为“开放模块”的原因。 考虑一个例子:
const myRevealingModule = (function() { let privateVar = 'Peter'; const publicVar = 'Hello World'; function privateFunction() { console.log('Name: '+ privateVar); } function publicSetName(name) { privateVar = name; } function publicGetName() { privateFunction(); } return { setName: publicSetName, greeting: publicVar, getName: publicGetName }; })(); myRevealingModule.setName('Mark');
应用此模式可以更容易地理解模块的哪些功能和变量是公开可用的,这有助于提高代码的可读性。
运行IIFE之后,
myRevealingModule
如下所示:
const myRevealingModule = { setName: publicSetName, greeting: publicVar, getName: publicGetName };
例如,我们可以调用
myRevealingModule.setName('Mark')
方法,该方法是对
publicSetName
内部函数的引用。
myRevealingModule.getName()
方法引用内部函数
publicGetName
。 例如:
myRevealingModule.setName('Mark');
考虑“开放模块”模式比“模块”模式的优势:
- “开放模块”使您可以公开公开模块的隐藏实体(并在必要时再次将其隐藏),对它们中的每一个仅修改IIFE之后返回的对象中的一行。
- 返回的对象不包含函数定义。 属性名称右边的所有内容均在IIFE中定义。 这有助于保持代码的清洁和易于阅读。
ES6中的模块
在发布ES6标准之前,JavaScript没有用于模块的标准工具;因此,开发人员必须使用第三方库或“模块”模式来实现适当的机制。 但是随着ES6的出现,JavaScript中出现了一个标准的模块系统。
ES6模块存储在文件中。 一个文件只能包含一个模块。 默认情况下,模块内部的所有内容都是私有的。 可以使用
export
关键字公开函数,变量和类。 模块内部的代码始终以严格模式执行。
▍导出模块
有两种方法可以导出模块中声明的函数或变量:
- 通过在声明函数或变量之前添加
export
关键字来完成export
。 例如:
// utils.js export const greeting = 'Hello World'; export function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } export function subtract(num1, num2) { console.log('Subtract:', num1, num2); return num1 - num2; } // - function privateLog() { console.log('Private Function'); }
- 通过在代码末尾添加
export
关键字来完成导出,该关键字列出了要导出的函数和变量的名称。 例如:
// utils.js function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } function divide(num1, num2) { console.log('Divide:', num1, num2); return num1 / num2; } // function privateLog() { console.log('Private Function'); } export {multiply, divide};
module导入模块
正如有两种导出方法一样,有两种导入模块的方法。 这可以使用
import
关键字完成:
- 导入多个所选项目。 例如:
// main.js // import { sum, multiply } from './utils.js'; console.log(sum(3, 7)); console.log(multiply(3, 7));
- 导入所有模块导出的内容。 例如:
// main.js // , import * as utils from './utils.js'; console.log(utils.sum(3, 7)); console.log(utils.multiply(3, 7));
exported进出口实体的别名
如果导出到代码中的函数或变量的名称可能导致冲突,则可以在导出或导入期间更改它们。
要在导出期间重命名实体,可以执行以下操作:
// utils.js function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } export {sum as add, multiply};
要在导入期间重命名实体,请使用以下构造:
// main.js import { add, multiply as mult } from './utils.js'; console.log(add(3, 7)); console.log(mult(3, 7));
单例模式
“ Singleton”或“ Singleton”模式是只能存在于单个副本中的对象。 作为此模式的应用程序的一部分,如果尚未创建类的新实例,则将创建它。 如果类实例已经存在,则在尝试访问构造函数时,将返回对相应对象的引用。 对构造函数的后续调用将始终返回相同的对象。
实际上,JavaScript中一直存在我们所谓的“单身”模式,但他们称其不是“单身”,而是“对象文字”。 考虑一个例子:
const user = { name: 'Peter', age: 25, job: 'Teacher', greet: function() { console.log('Hello!'); } };
由于JavaScript中的每个对象都占用其自己的内存区域,并且不与其他对象共享该区域,因此,每当访问
user
变量时,我们都会获得指向同一对象的链接。
可以使用构造函数来实现Singleton模式。 看起来像这样:
let instance = null; function User(name, age) { if(instance) { return instance; } instance = this; this.name = name; this.age = age; return instance; } const user1 = new User('Peter', 25); const user2 = new User('Mark', 24);
调用构造函数时,它首先检查
instance
对象是否存在。 如果相应的变量未初始化,则将其写入
instance
。 如果变量已经具有对对象的引用,则构造函数仅返回一个
instance
,即对现有对象的引用。
可以使用模块模式来实现Singleton模式。 例如:
const singleton = (function() { let instance; function User(name, age) { this.name = name; this.age = age; } return { getInstance: function(name, age) { if(!instance) { instance = new User(name, age); } return instance; } } })(); const user1 = singleton.getInstance('Peter', 24); const user2 = singleton.getInstance('Mark', 26);
在这里,我们通过调用
singleton.getInstance()
方法创建一个新的
user
实例。 如果对象的实例已经存在,则此方法将简单地返回它。 如果还没有这样的对象,则该方法通过调用
User
构造函数来创建它的新实例。
工厂模式
工厂模式使用所谓的工厂方法来创建对象。 您无需指定用于创建对象的类或构造函数。
在不需要公开其创建逻辑的情况下,此模式用于创建对象。 如果需要根据特定条件创建不同的对象,则可以使用Factory模式。 例如:
class Car{ constructor(options) { this.doors = options.doors || 4; this.state = options.state || 'brand new'; this.color = options.color || 'white'; } } class Truck { constructor(options) { this.doors = options.doors || 4; this.state = options.state || 'used'; this.color = options.color || 'black'; } } class VehicleFactory { createVehicle(options) { if(options.vehicleType === 'car') { return new Car(options); } else if(options.vehicleType === 'truck') { return new Truck(options); } } }
这里创建了
Car
和
Truck
类,它们提供了某些标准值的使用。 它们用于创建
car
和
truck
对象。 这里还声明了
VehicleFactory
类,该类用于基于对
vehicleType
属性的分析来创建新对象,并将
vehicleType
传递给对象的相应方法,该方法在
options
返回对象。 这是处理所有这些的方法:
const factory = new VehicleFactory(); const car = factory.createVehicle({ vehicleType: 'car', doors: 4, color: 'silver', state: 'Brand New' }); const truck= factory.createVehicle({ vehicleType: 'truck', doors: 2, color: 'white', state: 'used' }); // Car {doors: 4, state: "Brand New", color: "silver"} console.log(car); // Truck {doors: 2, state: "used", color: "white"} console.log(truck);
VehicleFactory
类的
factory
对象
VehicleFactory
。 之后,您可以通过调用
factory.createVehicle()
方法,并将其
vehicleType
属性设置为
car
或
truck
的
options
对象传递给
Car
或
Truck
类,来创建对象。
装饰图案
装饰器模式用于扩展对象的功能,而无需修改现有的类或构造函数。 此模式可用于向对象添加某些功能,而无需修改负责创建对象的代码。
这是使用此模式的简单示例:
function Car(name) { this.name = name; // this.color = 'White'; } // , const tesla= new Car('Tesla Model 3'); // - tesla.setColor = function(color) { this.color = color; } tesla.setPrice = function(price) { this.price = price; } tesla.setColor('black'); tesla.setPrice(49000); // black console.log(tesla.color);
现在让我们考虑应用该模式的实际示例。 假设汽车的成本取决于其功能,可用的附加功能。 如果不使用Decorator模式来描述这些汽车,我们将不得不为这些附加功能的不同组合创建不同的类,每个附加功能都将具有一种确定汽车成本的方法。 例如,它可能看起来像这样:
class Car() { } class CarWithAC() { } class CarWithAutoTransmission { } class CarWithPowerLocks { } class CarWithACandPowerLocks { }
借助所讨论的模式,您可以创建基本类
Car
,例如以基本配置描述一辆汽车,其成本由某个固定量表示。 之后,可以使用装饰器函数扩展基于此类创建的标准对象。 通过此功能处理的标准“汽车”会获得新的机会,此外,还会影响其价格。 例如,此方案可以实现如下:
class Car { constructor() { // this.cost = function() { return 20000; } } } // - function carWithAC(car) { car.hasAC = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } } // - function carWithAutoTransmission(car) { car.hasAutoTransmission = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 2000; } } // - function carWithPowerLocks(car) { car.hasPowerLocks = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } }
在这里,我们首先创建
Car
的基类,该基类用于创建代表
Car
对象。 然后,我们创建几个装饰器函数,使我们可以使用附加属性扩展
Car
类的对象。 这些函数将相应的对象作为参数。 之后,我们向对象添加一个新属性,指示该汽车将配备哪些新功能,然后重新定义该对象的
cost
函数,该函数现在返回汽车的新成本。 因此,为了为标准配置的汽车“装备”新的东西,我们可以使用以下设计:
const car = new Car(); console.log(car.cost()); carWithAC(car); carWithAutoTransmission(car); carWithPowerLocks(car);
之后,您可以找出经过改进的配置的汽车的成本:
// console.log(car.cost());
总结
在本文中,我们研究了JavaScript中使用的几种设计模式,但实际上,仍有许多模式可用于解决各种问题。
尽管了解各种设计模式对程序员很重要,但正确使用它们也同样重要。 程序员了解了模式及其应用范围后,通过分析前面的任务,可以了解哪种模式可以帮助解决该问题。
亲爱的读者们! 您最常使用哪种设计模式?
