本文概述
作为一名优秀的JavaScript开发人员, 你将努力编写干净, 健康且可维护的代码。你可以解决有趣的挑战, 尽管这些挑战是独特的, 但不一定需要独特的解决方案。你可能发现自己编写的代码看起来与你之前处理过的一个完全不同的问题的解决方案相似。你可能不知道, 但是你使用了JavaScript设计模式。设计模式是针对软件设计中常见问题的可重用解决方案。
在任何一种语言的生命周期中, 许多可重用的解决方案都是由该语言社区的大量开发人员制作和测试的。正是由于许多开发人员的综合经验, 此类解决方案之所以有用, 是因为它们可以帮助我们以优化的方式编写代码, 同时解决当前的问题。
我们从设计模式中获得的主要好处如下:
- 它们是行之有效的解决方案:由于许多开发人员经常使用设计模式, 因此可以肯定它们是可行的。不仅如此, 你可以确定对它们进行了多次修订并且可能已实施了优化。
- 它们易于重用:设计模式记录了可重用的解决方案, 可以将其修改为解决多个特定问题的方法, 因为它们不受特定问题的束缚。
- 它们具有表现力:设计模式可以相当优雅地解释大型解决方案。
- 它们使沟通更加轻松:当开发人员熟悉设计模式时, 他们可以更轻松地就特定问题的潜在解决方案彼此进行沟通。
- 它们避免了重构代码的需要:如果在编写应用程序时考虑了设计模式, 通常情况下, 以后就不需要重构代码, 因为将正确的设计模式应用于给定的问题已经是最佳选择了。解。
- 它们减小了代码库的大小:因为设计模式通常是优雅且最优的解决方案, 所以与其他解决方案相比, 它们通常需要更少的代码。
我知道你已经准备好进入这一阶段, 但是在你全面了解设计模式之前, 让我们先回顾一些JavaScript基础知识。
JavaScript简史
JavaScript是当今Web开发中最受欢迎的编程语言之一。最初, 它是为各种显示的HTML元素(一种称为客户端脚本语言)的”胶水”, 用于最初的Web浏览器之一。称为Netscape Navigator, 它当时只能显示静态HTML。你可能会想到, 这种脚本语言的想法在当时的浏览器开发行业的大公司之间引起了浏览器之战, 例如Netscape Communications(今天的Mozilla), Microsoft等。
每个大公司都希望通过自己的脚本语言实现自己的实现, 因此Netscape制作了JavaScript(实际上, Brendan Eich制作了JavaScript), Microsoft制作了JScript, 等等。如你所见, 这些实现之间的差异非常大, 因此针对每个浏览器进行了Web浏览器的开发, 并附带了网页附带的查看最佳的标签。很快就清楚了, 我们需要一个标准的跨浏览器解决方案, 该解决方案将统一开发过程并简化网页的创建。他们想出的就是ECMAScript。
ECMAScript是所有现代浏览器都试图支持的标准化脚本语言规范, 并且ECMAScript有多种实现(可以说方言)。最受欢迎的主题是JavaScript。自发布以来, ECMAScript已标准化了许多重要内容, 对于那些对特定内容更感兴趣的人, Wikipedia上提供了每种ECMAScript版本的标准化项目的详细列表。对ECMAScript版本6(ES6)和更高版本的浏览器支持仍不完整, 必须完全移植到ES5才能获得完全支持。
什么是JavaScript?
为了完全掌握本文的内容, 让我们介绍一些非常重要的语言特征, 在深入探讨JavaScript设计模式之前, 我们需要了解这些特征。如果有人问你”什么是JavaScript?”你可能会按照以下方式回答:
JavaScript是一种轻量级的, 解释性的, 面向对象的编程语言, 具有一流的功能, 通常被称为网页的脚本语言。
前面提到的定义意味着, JavaScript代码具有类似于C ++和Java之类的流行语言的语法, 具有较低的内存占用空间, 易于实现和易于学习。它是一种脚本语言, 这意味着将解释其代码, 而不是对其进行编译。它支持过程式, 面向对象和函数式编程样式, 这使其对开发人员非常灵活。
到目前为止, 我们已经看过了听起来像许多其他语言的所有特征, 因此让我们看一下关于其他语言的JavaScript的具体特征。我将列出一些特征, 并尽我最大的努力解释它们为什么值得特别注意。
JavaScript支持一流的功能
当我刚开始使用JavaScript时, 由于我来自C / C ++背景, 所以这个特性曾经使我难以掌握。 JavaScript将函数视为一等公民, 这意味着你可以像将任何其他变量一样将函数作为参数传递给其他函数。
// we send in the function as an argument to be
// executed from inside the calling function
function performOperation(a, b, cb) {
var c = a + b;
cb(c);
}
performOperation(2, 3, function(result) {
// prints out 5
console.log("The result of the operation is " + result);
})
JavaScript是基于原型的
与许多其他面向对象的语言一样, JavaScript支持对象, 而考虑对象时想到的第一个术语是类和继承。这是个棘手的问题, 因为该语言不支持其普通语言形式的类, 而是使用了基于原型的继承或基于实例的继承。
就在ES6中, 现在才引入了正式的术语类, 这意味着浏览器仍然不支持该类(如果你还记得, 在撰写本文时, 最后一个完全受支持的ECMAScript版本是5.1)。重要的是要注意, 尽管JavaScript中引入了”类”一词, 但它仍然利用了基于原型的继承。
基于原型的编程是一种面向对象的编程, 其中行为重用(称为继承)是通过作为原型的委托通过重用现有对象的过程来执行的。一旦进入本文的设计模式部分, 我们将对此进行更详细的介绍, 因为许多JavaScript设计模式都使用了此特性。
JavaScript事件循环
如果你有使用JavaScript的经验, 那么你一定对术语回调函数很熟悉。对于不熟悉该术语的人, 回调函数是作为参数发送给另一个函数的函数(请记住, JavaScript将函数视为一等公民), 并在事件触发后执行。通常用于订阅事件, 例如鼠标单击或键盘按钮按下。
每次附加了侦听器的事件触发(否则该事件丢失), 都会以FIFO方式将消息发送到正在同步处理的消息队列中(先进先出) )。这称为事件循环。
队列上的每个消息都有与其关联的功能。一旦消息出队, 运行时将在处理任何其他消息之前完全执行该功能。这就是说, 如果一个函数包含其他函数调用, 则它们都将在处理队列中的新消息之前执行。这称为运行完成。
while (queue.waitForMessage()) {
queue.processNextMessage();
}
queue.waitForMessage()同步等待新消息。每个要处理的消息都有其自己的堆栈, 并被处理直到堆栈为空。完成后, 将从队列中处理一条新消息(如果有)。
你可能还听说过JavaScript是非阻塞的, 这意味着当执行异步操作时, 程序能够在等待异步操作完成的同时处理其他事情, 例如接收用户输入, 而不阻塞主程序。执行线程。这是JavaScript的一个非常有用的属性, 整篇文章都可以就此主题撰写。但是, 它不在本文的讨论范围之内。
什么是设计模式?
如前所述, 设计模式是解决软件设计中常见问题的可重用解决方案。让我们看一下一些设计模式类别。
原型模式
如何创建模式?假设你已经认识到一个普遍存在的问题, 并且你拥有针对该问题的独特解决方案, 但该解决方案尚未得到全球的认可和记录。每次遇到此问题时, 你都使用此解决方案, 并且认为它是可重用的, 并且开发人员社区可以从中受益。
它会立即成为一种模式吗?幸运的是, 没有。通常, 人们可能具有良好的代码编写习惯, 并且在实际上不是某种模式的情况下, 只是将看起来像某种模式的东西误认为一种模式。
你如何知道自己认为是设计模式的时间?
通过征询其他开发人员的意见, 了解自身创建模式的过程以及使自己熟悉现有模式。模式成为完整模式之前必须经过一个阶段, 这称为原型模式。
原型模式是经过各种开发人员和场景证明经过一定时间测试的未来模式, 其中该模式被证明是有用的并给出正确的结果。为了使社区认可一个成熟的模式, 有大量的工作和文档(其中大部分不在本文的讨论范围之内)。
反模式
由于设计模式代表良好实践, 因此反模式代表不良实践。
反模式的一个示例是修改Object类原型。 JavaScript中几乎所有对象都从Object继承(请记住JavaScript使用基于原型的继承), 因此请设想一下你更改此原型的情况。从该原型继承的所有对象(大多数JavaScript对象)中都可以看到对Object原型的更改。这是一场灾难, 等待发生。
与上述示例类似, 另一个示例是修改你不拥有的对象。这样的一个示例是从整个应用程序中许多场景中使用的对象覆盖功能。如果你与一个大型团队合作, 请想象这会造成混乱;你会很快遇到命名冲突, 不兼容的实现以及维护方面的噩梦。
类似于了解所有良好实践和解决方案的有用方式一样, 了解不良实践和解决方案也非常重要。这样, 你可以识别它们, 并避免预先犯错。
设计模式分类
设计模式可以通过多种方式进行分类, 但是最受欢迎的一种是以下几种:
- 创意设计模式
- 结构设计模式
- 行为设计模式
- 并发设计模式
- 建筑设计模式
创新设计模式
与基本方法相比, 这些模式处理的对象创建机制可优化对象创建。对象创建的基本形式可能会导致设计问题或增加设计的复杂性。创新设计模式通过某种方式控制对象创建来解决此问题。此类别中的一些流行设计模式是:
- 工厂方法
- 抽象工厂
- 建造者
- 原型
- 辛格尔顿
结构设计模式
这些模式处理对象关系。他们确保, 如果系统的一部分发生更改, 则整个系统都不需要随之更改。此类别中最受欢迎的模式是:
- 适配器
- 桥
- 综合
- 装饰器
- 正面
- 飞行重量
- 代理
行为设计模式
这些类型的模式可以识别, 实现和改善系统中不同对象之间的通信。它们有助于确保系统的各个部分具有同步的信息。这些模式的流行示例是:
- 责任链
- 命令
- 迭代器
- 调解员
- 纪念品
- 观察者
- 州
- 战略
- 游客
并发设计模式
这些类型的设计模式处理多线程编程范例。一些受欢迎的是:
- 活动对象
- 核反应
- 排程器
建筑设计模式
用于建筑目的的设计模式。一些最著名的是:
- MVC(模型-视图-控制器)
- MVP(模型视图呈现器)
- MVVM(模型-视图-视图模型)
在下一节中, 我们将通过提供示例以更好地理解来仔细研究上述一些设计模式。
设计模式示例
每个设计模式都代表针对特定类型问题的特定解决方案类型。没有通用的模式集总是最合适的。我们需要学习何时一种特定的模式将证明有用, 以及是否将提供实际的价值。一旦我们熟悉了它们最适合的模式和场景, 就可以轻松确定特定模式是否适合特定问题。
请记住, 将错误的模式应用于给定的问题可能会导致不良后果, 例如不必要的代码复杂性, 不必要的性能开销, 甚至产生新的反模式。
这些都是在考虑将设计模式应用于我们的代码时要考虑的所有重要事项。我们将看一些我个人认为有用的设计模式, 并相信每个高级JavaScript开发人员都应该熟悉。
构造器模式
考虑经典的面向对象的语言时, 构造函数是类中的特殊功能, 该类使用一组默认值和/或发送值初始化对象。
在JavaScript中创建对象的常用方法是以下三种方法:
// either of the following ways can be used to create a new object
var instance = {};
// or
var instance = Object.create(Object.prototype);
// or
var instance = new Object();
创建对象后, 有四种方法(自ES3起)将属性添加到这些对象。它们是:
// supported since ES3
// the dot notation
instance.key = "A key's value";
// the square brackets notation
instance["key"] = "A key's value";
// supported since ES5
// setting a single property using Object.defineProperty
Object.defineProperty(instance, "key", {
value: "A key's value", writable: true, enumerable: true, configurable: true
});
// setting multiple properties using Object.defineProperties
Object.defineProperties(instance, {
"firstKey": {
value: "First key's value", writable: true
}, "secondKey": {
value: "Second key's value", writable: false
}
});
创建对象的最流行方法是大括号, 对于添加属性, 请使用点符号或方括号。任何具有JavaScript经验的人都可以使用它们。
前面我们提到过, JavaScript不支持本机类, 但是通过使用在函数调用前面加上” new”关键字来支持构造函数。这样, 我们可以将函数用作构造函数, 并像使用经典语言构造函数一样初始化其属性。
// we define a constructor for Person objects
function Person(name, age, isDeveloper) {
this.name = name;
this.age = age;
this.isDeveloper = isDeveloper || false;
this.writesCode = function() {
console.log(this.isDeveloper? "This person does write code" : "This person does not write code");
}
}
// creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode
var person1 = new Person("Bob", 38, true);
// creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode
var person2 = new Person("Alice", 32);
// prints out: This person does write code
person1.writesCode();
// prints out: this person does not write code
person2.writesCode();
但是, 这里仍有改进的空间。你可能还记得, 我之前提到过JavaScript使用基于原型的继承。前一种方法的问题在于, 为Person构造函数的每个实例重新定义了writesCode方法。我们可以通过将方法设置到函数原型中来避免这种情况:
// we define a constructor for Person objects
function Person(name, age, isDeveloper) {
this.name = name;
this.age = age;
this.isDeveloper = isDeveloper || false;
}
// we extend the function's prototype
Person.prototype.writesCode = function() {
console.log(this.isDeveloper? "This person does write code" : "This person does not write code");
}
// creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode
var person1 = new Person("Bob", 38, true);
// creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode
var person2 = new Person("Alice", 32);
// prints out: This person does write code
person1.writesCode();
// prints out: this person does not write code
person2.writesCode();
现在, Person构造函数的两个实例都可以访问writesCode()方法的共享实例。
模块模式
就特性而言, JavaScript永远不会停止惊奇。 JavaScript的另一特质(至少就面向对象语言而言)是JavaScript不支持访问修饰符。在经典的OOP语言中, 用户定义一个类并确定其成员的访问权限。由于纯格式的JavaScript不支持类或访问修饰符, 因此JavaScript开发人员找到了一种在需要时模仿此行为的方法。
在介绍模块模式细节之前, 让我们谈谈闭包的概念。闭包是即使父函数已关闭, 也可以访问父作用域的函数。它们通过作用域来帮助我们模仿访问修饰符的行为。让我们通过一个示例展示一下:
// we used an immediately invoked function expression
// to create a private variable, counter
var counterIncrementer = (function() {
var counter = 0;
return function() {
return ++counter;
};
})();
// prints out 1
console.log(counterIncrementer());
// prints out 2
console.log(counterIncrementer());
// prints out 3
console.log(counterIncrementer());
如你所见, 通过使用IIFE, 我们将计数器变量绑定到了一个已调用和关闭的函数上, 但仍可以由递增该子变量的子函数访问。由于我们无法从函数表达式的外部访问计数器变量, 因此我们通过范围操作将其设为私有。
使用闭包, 我们可以创建带有私有和公共部分的对象。这些被称为模块, 每当我们要隐藏对象的某些部分并仅向模块用户公开接口时, 它们就非常有用。让我们在一个例子中展示一下:
// through the use of a closure we expose an object
// as a public API which manages the private objects array
var collection = (function() {
// private members
var objects = [];
// public members
return {
addObject: function(object) {
objects.push(object);
}, removeObject: function(object) {
var index = objects.indexOf(object);
if (index >= 0) {
objects.splice(index, 1);
}
}, getObjects: function() {
return JSON.parse(JSON.stringify(objects));
}
};
})();
collection.addObject("Bob");
collection.addObject("Alice");
collection.addObject("Franck");
// prints ["Bob", "Alice", "Franck"]
console.log(collection.getObjects());
collection.removeObject("Alice");
// prints ["Bob", "Franck"]
console.log(collection.getObjects());
这种模式引入的最有用的功能是对象的私有部分和公共部分的清晰分离, 这是与来自经典面向对象背景的开发人员非常相似的概念。
但是, 并非一切都那么完美。当你希望更改成员的可见性时, 由于访问公共部分和私有部分的性质不同, 你需要在使用该成员的任何地方修改代码。同样, 创建后添加到对象的方法不能访问对象的私有成员。
显示模块模式
该图案是对如上所述的模块图案的改进。主要区别在于, 我们在模块的私有范围内编写了整个对象逻辑, 然后通过返回一个匿名对象简单地公开了我们希望公开的部分。当将私人成员映射到其相应的公共成员时, 我们还可以更改私人成员的命名。
// we write the entire object logic as private members and
// expose an anonymous object which maps members we wish to reveal
// to their corresponding public members
var namesCollection = (function() {
// private members
var objects = [];
function addObject(object) {
objects.push(object);
}
function removeObject(object) {
var index = objects.indexOf(object);
if (index >= 0) {
objects.splice(index, 1);
}
}
function getObjects() {
return JSON.parse(JSON.stringify(objects));
}
// public members
return {
addName: addObject, removeName: removeObject, getNames: getObjects
};
})();
namesCollection.addName("Bob");
namesCollection.addName("Alice");
namesCollection.addName("Franck");
// prints ["Bob", "Alice", "Franck"]
console.log(namesCollection.getNames());
namesCollection.removeName("Alice");
// prints ["Bob", "Franck"]
console.log(namesCollection.getNames());
显示模块模式是实现模块模式的至少三种方式之一。显示模块模式与模块模式的其他变体之间的区别主要在于如何引用公共成员。结果, 显示模块模式更易于使用和修改。但是, 在某些情况下它可能会变得脆弱, 例如在继承链中使用RMP对象作为原型。有问题的情况如下:
- 如果我们有一个私有函数引用一个公共函数, 那么我们就不能覆盖该公共函数, 因为私有函数将继续引用该函数的私有实现, 从而在我们的系统中引入了一个错误。
- 如果我们有一个指向私有变量的公共成员, 并尝试从模块外部覆盖该公共成员, 则其他函数仍将引用该变量的私有值, 从而在我们的系统中引入了一个错误。
单例模式
单例模式用于需要一个类的一个实例的场景。例如, 我们需要一个包含某些配置的对象。在这些情况下, 只要系统中某处需要配置对象, 就不必创建新对象。
var singleton = (function() {
// private singleton value which gets initialized only once
var config;
function initializeConfiguration(values){
this.randomNumber = Math.random();
values = values || {};
this.number = values.number || 5;
this.size = values.size || 10;
}
// we export the centralized method for retrieving the singleton value
return {
getConfig: function(values) {
// we initialize the singleton value only once
if (config === undefined) {
config = new initializeConfiguration(values);
}
// and return the same config value wherever it is asked for
return config;
}
};
})();
var configObject = singleton.getConfig({ "size": 8 });
// prints number: 5, size: 8, randomNumber: someRandomDecimalValue
console.log(configObject);
var configObject1 = singleton.getConfig({ "number": 8 });
// prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config
console.log(configObject1);
在示例中可以看到, 生成的随机数以及发送的配置值始终相同。
重要的是要注意, 用于检索单例值的访问点仅需一个并且是众所周知的。使用此模式的缺点是很难测试。
观察者模式
当我们需要以优化的方式改善系统不同部分之间的通信时, 观察者模式是一个非常有用的工具。它促进了对象之间的松散耦合。
此模式有多种版本, 但以其最基本的形式, 我们有该模式的两个主要部分。第一个是主题, 第二个是观察者。
主题负责处理与观察者订阅的特定主题有关的所有操作。这些操作使观察者订阅某个主题, 从某个主题取消订阅某个观察者, 并在事件发布时向观察者通知某个主题。
但是, 这种模式有一个变体, 称为发布者/订阅者模式, 在本节中, 我将使用它作为示例。经典观察者模式和发布者/订阅者模式之间的主要区别在于, 发布者/订阅者比观察者模式更促进了松散耦合。
在观察者模式中, 主题持有对订阅的观察者的引用, 并直接从对象本身调用方法, 而在发布者/订阅者模式中, 我们具有充当订阅者和发布者之间的沟通桥梁的渠道。发布者触发一个事件, 并简单地执行为该事件发送的回调函数。
我将显示发布者/订阅者模式的简短示例, 但是对于那些感兴趣的人, 可以在网上轻松找到经典的观察者模式示例。
var publisherSubscriber = {};
// we send in a container object which will handle the subscriptions and publishings
(function(container) {
// the id represents a unique subscription id to a topic
var id = 0;
// we subscribe to a specific topic by sending in
// a callback function to be executed on event firing
container.subscribe = function(topic, f) {
if (!(topic in container)) {
container[topic] = [];
}
container[topic].push({
"id": ++id, "callback": f
});
return id;
}
// each subscription has its own unique ID, which we use
// to remove a subscriber from a certain topic
container.unsubscribe = function(topic, id) {
var subscribers = [];
for (var subscriber of container[topic]) {
if (subscriber.id !== id) {
subscribers.push(subscriber);
}
}
container[topic] = subscribers;
}
container.publish = function(topic, data) {
for (var subscriber of container[topic]) {
// when executing a callback, it is usually helpful to read
// the documentation to know which arguments will be
// passed to our callbacks by the object firing the event
subscriber.callback(data);
}
}
})(publisherSubscriber);
var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) {
console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data));
});
var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) {
console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data));
});
var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) {
console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data));
});
// NOTE: after publishing an event with its data, all of the
// subscribed callbacks will execute and will receive
// a data object from the object firing the event
// there are 3 console.logs executed
publisherSubscriber.publish("mouseClicked", {"data": "data1"});
publisherSubscriber.publish("mouseHovered", {"data": "data2"});
// we unsubscribe from an event by removing the subscription ID
publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3);
// there are 2 console.logs executed
publisherSubscriber.publish("mouseClicked", {"data": "data1"});
publisherSubscriber.publish("mouseHovered", {"data": "data2"});
当我们需要对一个被触发的事件执行多个操作时, 这种设计模式非常有用。假设你有一个场景, 我们需要对后端服务进行多个AJAX调用, 然后根据结果执行其他AJAX调用。你可能必须将AJAX调用一个嵌套在另一个嵌套中, 这可能会导致称为回调地狱的情况。使用发布者/订阅者模式是一种更为优雅的解决方案。
使用此模式的缺点是很难测试系统的各个部分。没有一种优雅的方法让我们知道系统的预订部分是否表现出预期。
中介者模式
我们将简要介绍一个模式, 这在谈论解耦系统时也非常有用。当我们遇到需要系统的多个部分进行通信和协调的情况时, 也许一个好的解决方案是引入一个中介器。
中介程序是一个对象, 用作系统不同部分之间通信的中心点, 并处理它们之间的工作流程。现在, 必须强调它可以处理工作流程。为什么这很重要?
因为与发布者/订阅者模式有很大的相似性。你可能会问自己, 好吧, 所以这两种模式都有助于实现对象之间更好的通信……有什么区别?
区别在于, 调解员负责工作流, 而发布者/订阅者使用一种称为”即发即弃”的通信方式。发布者/订阅者仅仅是事件聚合器, 这意味着它只是负责触发事件并让正确的订阅者知道触发了哪些事件。事件聚合器不在乎触发事件后会发生什么, 而调解器则不会。
中介程序的一个很好的例子是向导类型的界面。假设你使用的系统的注册过程很繁琐。通常, 当需要用户提供大量信息时, 将其分解为多个步骤是一种好习惯。
这样, 代码将更加整洁(更易于维护), 并且用户不会为完成注册所要求的信息量感到不知所措。中介者是一个将处理注册步骤的对象, 同时考虑到由于每个用户可能具有唯一的注册过程而可能发生的不同工作流程。
这种设计模式的明显好处是改善了系统不同部分之间的通信, 这些部分现在都通过中介程序和更干净的代码库进行通信。
不利的一面是, 现在我们已经在系统中引入了单点故障, 这意味着如果我们的中介程序发生故障, 则整个系统可能会停止工作。
原型模式
正如我们在整篇文章中已经提到的那样, JavaScript不支持其本机形式的类。对象之间的继承是使用基于原型的编程实现的。
它使我们能够创建可以作为其他正在创建的对象原型的对象。原型对象用作构造函数创建的每个对象的蓝图。
正如我们在前面的部分中已经讨论过的那样, 让我们展示一个简单的示例说明如何使用此模式。
var personPrototype = {
sayHi: function() {
console.log("Hello, my name is " + this.name + ", and I am " + this.age);
}, sayBye: function() {
console.log("Bye Bye!");
}
};
function Person(name, age) {
name = name || "John Doe";
age = age || 26;
function constructorFunction(name, age) {
this.name = name;
this.age = age;
};
constructorFunction.prototype = personPrototype;
var instance = new constructorFunction(name, age);
return instance;
}
var person1 = Person();
var person2 = Person("Bob", 38);
// prints out Hello, my name is John Doe, and I am 26
person1.sayHi();
// prints out Hello, my name is Bob, and I am 38
person2.sayHi();
请注意, 原型继承如何也可以提高性能, 因为两个对象都包含对在原型本身而不是每个对象中实现的功能的引用。
命令模式
当我们想将执行命令的对象与发出命令的对象分离时, 命令模式很有用。例如, 设想一个场景, 其中我们的应用程序正在使用大量API服务调用。然后, 假设API服务发生了变化。无论调用了什么API, 我们都必须修改代码。
这将是实现抽象层的好地方, 该抽象层会将调用API服务的对象与告诉它们何时调用API服务的对象分开。这样, 我们避免在需要调用服务的所有地方进行修改, 而只需要更改进行调用本身的对象(这只是一个地方)。
与任何其他模式一样, 我们必须知道何时真正需要这种模式。我们需要意识到我们正在做出的权衡, 因为我们在API调用上添加了一个额外的抽象层, 这将降低性能, 但在需要修改执行命令的对象时可能会节省大量时间。
// the object which knows how to execute the command
var invoker = {
add: function(x, y) {
return x + y;
}, subtract: function(x, y) {
return x - y;
}
}
// the object which is used as an abstraction layer when
// executing commands; it represents an interface
// toward the invoker object
var manager = {
execute: function(name, args) {
if (name in invoker) {
return invoker[name].apply(invoker, [].slice.call(arguments, 1));
}
return false;
}
}
// prints 8
console.log(manager.execute("add", 3, 5));
// prints 2
console.log(manager.execute("subtract", 5, 3));
外墙图案
当我们想要在公开显示的内容与幕后实现的内容之间创建抽象层时, 将使用立面模式。当需要更简单或更简单的基础对象接口时使用。
这种模式的一个很好的例子是来自DOM操作库(如jQuery, Dojo或D3)的选择器。你可能已经注意到使用这些库, 它们具有非常强大的选择器功能。你可以编写复杂的查询, 例如:
jQuery(".parent .child div.span")
它极大地简化了选择功能, 尽管表面上看起来很简单, 但为了实现此目的, 在引擎盖下实施了整个复杂的逻辑。
我们还需要注意性能简单性的权衡。如果不够用的话, 最好避免额外的复杂性。在上述库的情况下, 值得权衡, 因为它们都是非常成功的库。
下一步
设计模式是任何高级JavaScript开发人员都应该意识到的非常有用的工具。知道有关设计模式的细节可能会非常有用, 并且可以在任何项目的生命周期(尤其是维护部分)中节省大量时间。在设计模式的帮助下修改和维护非常适合系统需求的系统可能被证明是无价的。
为了使文章相对简短, 我们将不再显示任何示例。对于那些感兴趣的人来说, 这篇文章的灵感来自《四大模式的设计模式:可重用的面向对象软件的元素》和Addy Osmani的《学习JavaScript设计模式》。我强烈推荐两本书。
相关:作为JS开发人员, 这就是让我彻夜难眠/弄清ES6类混乱的原因