作为一名优秀的 JavaScript 开发人员,你会努力编写干净、健康且可维护的代码。你解决了有趣的挑战,这些挑战虽然独特,但不一定需要独特的解决方案。你可能发现自己编写的代码看起来与你之前处理过的一个完全不同的问题的解决方案相似。你可能不知道,但你已经使用过 JavaScript设计模式。设计模式是软件设计中常见问题的可重用解决方案,本文还包括详细的JavaScript设计模式用法示例。
在任何语言的生命周期中,许多此类可重用解决方案是由该语言社区的大量开发人员制定和测试的。正是由于许多开发人员的这种综合经验,这些解决方案如此有用,因为它们可以帮助我们以优化的方式编写代码,同时解决手头的问题。
我们从设计模式中获得的主要好处如下:
- 它们是经过验证的解决方案:因为许多开发人员经常使用设计模式,所以你可以肯定它们是有效的。不仅如此,你还可以确定它们经过多次修订,并且可能实施了优化。
- 它们易于重用:设计模式记录了一个可重用的解决方案,可以对其进行修改以解决多个特定问题,因为它们与特定问题无关。
- 它们富有表现力:设计模式可以非常优雅地解释大型解决方案。
- 它们简化了交流:当开发人员熟悉设计模式时,他们可以更轻松地相互交流关于给定问题的潜在解决方案。
- 它们避免了重构代码的需要:如果应用程序在编写时考虑了设计模式,通常情况下你以后不需要重构代码,因为将正确的设计模式应用于给定的问题已经是最佳的解决方案。
- 它们降低了代码库的大小:因为设计模式通常是优雅和最佳的解决方案,所以它们通常需要比其他解决方案更少的代码。
我知道你现在已经准备好开始学习了,但是在你了解所有设计模式之前,让我们先回顾一些 JavaScript 基础知识。
JavaScript设计模式教程:JavaScript 简史
JavaScript 是当今最流行的 Web 开发编程语言之一。它最初是作为一种用于各种显示 HTML 元素的“胶水”,称为客户端脚本语言,用于初始 Web 浏览器之一。被称为 Netscape Navigator,它当时只能显示静态 HTML。正如你可能认为的那样,这种脚本语言的想法导致了当时浏览器开发行业的主要参与者之间的浏览器战争,例如 Netscape Communications(今天的 Mozilla)、Microsoft 和其他公司。
每个大玩家都想推动他们自己实现这种脚本语言,所以 Netscape 开发了 JavaScript(实际上,Brendan Eich 做了),Microsoft 开发了 JScript,等等。正如你所想象的那样,这些实现之间的差异很大,因此针对 Web 浏览器的开发是针对每个浏览器进行的,并带有网页附带的最佳浏览贴纸。很快就很明显我们需要一个标准的、跨浏览器的解决方案来统一开发过程并简化网页的创建。他们想出的东西叫做ECMAScript。
ECMAScript 是一种标准化的脚本语言规范,所有现代浏览器都试图支持它,并且有多种 ECMAScript 实现(可以说是方言)。最受欢迎的就是本文的主题,JavaScript。自最初发布以来,ECMAScript 已经标准化了很多重要的东西,对于那些对细节更感兴趣的人,维基百科上提供了每个版本的 ECMAScript 标准化项目的详细列表。浏览器对 ECMAScript 版本 6 (ES6) 及更高版本的支持仍然不完整,必须转换为 ES5 才能得到完全支持。
什么是 JavaScript?
JavaScript如何使用设计模式?为了全面掌握本文的内容,我们先介绍一些非常重要的语言特性,在深入研究 JavaScript 设计模式之前,我们需要了解这些特性。如果有人问你“什么是 JavaScript?” 你可能会在以下几行中回答:
JavaScript 是一种轻量级、解释型、面向对象的编程语言,具有一流的功能,最常被称为网页脚本语言。
上述定义的意思是说 JavaScript 代码内存占用低,易于实现,易于学习,语法类似于 C++ 和 Java 等流行语言。它是一种脚本语言,这意味着它的代码被解释而不是编译。它支持过程式、面向对象和函数式编程风格,这使得开发人员非常灵活。
到目前为止,我们已经了解了所有听起来与许多其他语言相似的特征,所以让我们来看看 JavaScript 与其他语言相比有何特殊之处。我将列出一些特征,并尽我所能解释为什么它们值得特别关注。
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 中,正式术语class被引入,这意味着浏览器仍然不支持这个(如果你还记得,在撰写本文时,最后一个完全支持的 ECMAScript 版本是 5.1)。然而,重要的是要注意,即使术语“类”被引入到 JavaScript 中,它仍然在底层使用基于原型的继承。
基于原型的编程是一种面向对象的编程风格,其中行为重用(称为继承)是通过作为原型的委托重用现有对象的过程来执行的。一旦我们进入本文的设计模式部分,我们将深入研究更多细节,因为该特性用于许多 JavaScript 设计模式。
JavaScript 事件循环
如果你有使用 JavaScript 的经验,那么你肯定熟悉术语回调函数。对于那些不熟悉这个术语的人来说,回调函数是作为参数(请记住,JavaScript 将函数视为一等公民)发送到另一个函数并在事件触发后执行的函数。这通常用于订阅诸如鼠标单击或键盘按钮按下等事件。
每次触发一个带有监听器的事件(否则事件丢失)时,都会以先进先出的方式(先进先出)将消息发送到正在同步处理的消息队列中)。这称为事件循环。
队列中的每条消息都有一个与之关联的函数。消息出列后,运行时会在处理任何其他消息之前完全执行该函数。也就是说,如果一个函数包含其他函数调用,它们都在处理来自队列的新消息之前执行。这称为运行到完成。
while (queue.waitForMessage()) {
queue.processNextMessage();
}
该queue.waitForMessage()
同步等待新的消息。正在处理的每个消息都有自己的堆栈,并被处理直到堆栈为空。一旦完成,就会从队列中处理一条新消息(如果有的话)。
你可能也听说过 JavaScript 是非阻塞的,这意味着当一个异步操作正在执行时,程序能够处理其他事情,比如接收用户输入,同时等待异步操作完成,而不是阻塞主执行线程。这是 JavaScript 的一个非常有用的特性,可以写一整篇文章来讨论这个主题;但是,这超出了本文的范围。
什么是设计模式?
正如我之前所说,设计模式是软件设计中常见问题的可重用解决方案。让我们来看看设计模式的一些类别。
原型模式
一个人如何创造一个模式?假设你认识到了一个普遍存在的问题,并且你对此问题有自己独特的解决方案,但该解决方案并未在全球范围内得到认可和记录。每次遇到此问题时,你都会使用此解决方案,并且你认为它是可重用的,并且开发人员社区可以从中受益。
它会立即成为一种模式吗?幸运的是,没有。很多时候,一个人可能有良好的代码编写习惯,只是简单地将一些看起来像一种模式的东西误认为是一种模式,而实际上它不是一种模式。
你怎么知道什么时候你认为你认识的实际上是一种设计模式?
通过获取其他开发人员的意见,了解创建模式本身的过程,并使自己熟悉现有模式。模式在成为成熟模式之前必须经历一个阶段,这称为原型模式。
如果原型模式通过了各种开发人员和场景的一段时间测试,证明该模式有用并给出正确的结果,那么它就是一种未来模式。有大量的工作和文档——其中大部分超出了本文的范围——需要完成,以便让社区认可一个成熟的模式。
反模式
设计模式代表好的实践,反模式代表坏的实践。
反模式的一个例子是修改Object
类原型。JavaScript 中的几乎所有对象都继承自Object
(请记住,JavaScript 使用基于原型的继承),因此想象一下你更改此原型的场景。对Object
原型的更改将在从该原型继承的所有对象中看到——这将是大多数JavaScript 对象。这是一场等待发生的灾难。
另一个与上述类似的示例是修改你不拥有的对象。这方面的一个例子是覆盖整个应用程序中许多场景中使用的对象的函数。如果你与一个大型团队一起工作,想象一下这会造成什么混乱;你很快就会遇到命名冲突、不兼容的实现和维护噩梦。
与了解所有好的做法和解决方案的有用性类似,了解坏的做法和解决方案也非常重要。这样,你可以识别它们并避免预先犯错。
设计模式分类
设计模式可以通过多种方式进行分类,但最流行的是以下一种:
- 创造性的设计模式
- 结构设计模式
- 行为设计模式
- 并发设计模式
- 架构设计模式
JavaScript设计模式用法示例:创建型设计模式
这些模式处理与基本方法相比优化对象创建的对象创建机制。对象创建的基本形式可能会导致设计问题或增加设计的复杂性。创建型设计模式通过以某种方式控制对象创建来解决这个问题。此类别中的一些流行设计模式是:
- 工厂方法
- 抽象工厂
- 建造者
- 原型
- 单例
结构设计模式
这些模式处理对象关系。他们确保如果系统的某一部分发生变化,则整个系统不需要随之变化。此类别中最受欢迎的模式是:
- 适配器
- 桥接
- 合成的
- 装饰器
- 门面
- 享元
- 代理
JavaScript设计模式教程:行为设计模式
这些类型的模式识别、实现和改进系统中不同对象之间的通信。它们有助于确保系统的不同部分具有同步信息。这些模式的流行示例是:
- 责任链
- 命令
- 迭代器
- 中介者
- 备忘
- 观察者
- 状态
- 战略
- 访问者
并发设计模式
这些类型的设计模式处理多线程编程范式。一些流行的是:
- 主动对象
- 核反应
- 调度器
架构设计模式
用于架构目的的设计模式。其中一些最著名的是:
- MVC(模型-视图-控制器)
- MVP(模型-视图-演示者)
- MVVM(模型-视图-视图模型)
在下一节中,我们将通过示例来仔细研究上述一些设计模式,以便更好地理解。
JavaScript设计模式用法示例
每个设计模式都代表针对特定类型问题的特定类型的解决方案。没有任何通用的模式总是最适合的。我们需要了解特定模式何时证明有用以及它是否会提供实际价值。一旦我们熟悉了它们最适合的模式和场景,我们就可以轻松确定特定模式是否适合给定问题。
请记住,将错误的模式应用于给定的问题可能会导致不良影响,例如不必要的代码复杂性、不必要的性能开销,甚至产生新的反模式。
在考虑将设计模式应用于我们的代码时,这些都是需要考虑的重要事项。我们将看看一些我个人认为有用的设计模式,相信每个高级 JavaScript 开发人员都应该熟悉。
构造器模式
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 使用基于原型的继承。前一种方法的问题在于该方法writesCode
会为Person
构造函数的每个实例重新定义。我们可以通过将方法设置到函数原型中来避免这种情况:
// 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());
这种模式引入的最有用的东西是对象的私有和公共部分的明确分离,这个概念与来自经典面向对象背景的开发人员非常相似。
然而,并非一切都如此完美。当你希望更改成员的可见性时,由于访问公共部分和私有部分的不同性质,你需要在使用此成员的任何地方修改代码。此外,创建后添加到对象的方法无法访问对象的私有成员。
揭示模块模式
JavaScript如何使用设计模式?这种模式是对上述模块模式的改进。主要区别在于我们在模块的私有范围内编写了整个对象逻辑,然后通过返回一个匿名对象来简单地公开我们想要公开的部分。我们还可以在将私有成员映射到相应的公共成员时更改私有成员的命名。
// 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 对象作为继承链中的原型。有问题的情况如下:
- 如果我们有一个引用公共函数的私有函数,我们不能覆盖公共函数,因为私有函数将继续引用函数的私有实现,从而将错误引入我们的系统。
- 如果我们有一个指向私有变量的公共成员,并尝试从模块外部覆盖公共成员,其他函数仍然会引用该变量的私有值,从而将错误引入我们的系统。
JavaScript设计模式教程:单例模式
当我们只需要一个类的一个实例时,就会使用单例模式。例如,我们需要一个包含某些配置的对象。在这些情况下,只要系统中某处需要配置对象,就没有必要创建新对象。
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);
正如你在示例中看到的,生成的随机数和发送的配置值始终相同。
需要注意的是,用于检索单例值的访问点只需一个并且非常知名。使用这种模式的一个缺点是它很难测试。
JavaScript设计模式用法示例:观察者模式
当我们需要以优化的方式改善系统不同部分之间的通信时,观察者模式是一个非常有用的工具。它促进了对象之间的松散耦合。
这种模式有多种版本,但在最基本的形式中,我们有该模式的两个主要部分。第一个是主体,第二个是观察者。
一个主题处理关于观察者订阅的某个主题的所有操作。这些操作使观察者订阅某个主题,取消订阅某个主题的观察者,并在发布事件时通知观察者有关某个主题的信息。
但是,这种模式有一个变体,称为发布者/订阅者模式,我将在本节中用作示例。经典观察者模式和发布者/订阅者模式之间的主要区别在于,发布者/订阅者促进了比观察者模式更松散的耦合。
在观察者模式中,主体持有对订阅观察者的引用,并直接从对象本身调用方法,而在发布者/订阅者模式中,我们有渠道,作为订阅者和发布者之间的通信桥梁。发布者触发一个事件并简单地执行为该事件发送的回调函数。
我将展示一个发布者/订阅者模式的简短示例,但对于那些感兴趣的人,可以在网上轻松找到经典的观察者模式示例。
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();
请注意原型继承如何提高性能,因为两个对象都包含对在原型本身中实现的函数的引用,而不是在每个对象中。
命令模式
JavaScript设计模式用法示例:当我们想要将执行命令的对象与发出命令的对象分离时,命令模式很有用。例如,假设我们的应用程序使用大量 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));
外观模式
JavaScript如何使用设计模式?当我们想要在公开展示的内容和幕后实现的内容之间创建一个抽象层时,可以使用外观模式。当需要一个更简单或更简单的底层对象接口时使用它。
这种模式的一个很好的例子是来自 DOM 操作库(如 jQuery、Dojo 或 D3)的选择器。你可能已经注意到使用这些库时,它们具有非常强大的选择器功能;你可以编写复杂的查询,例如:
jQuery(".parent .child div.span")
它大大简化了选择功能,尽管表面上看起来很简单,但为了使其工作,在引擎盖下实现了一个完整的复杂逻辑。
我们还需要注意性能与简单性之间的权衡。如果它不够有益,最好避免额外的复杂性。对于上述库,权衡是值得的,因为它们都是非常成功的库。
JavaScript设计模式教程总结
设计模式是一个非常有用的工具,任何高级 JavaScript 开发人员都应该知道。了解有关设计模式的细节可以证明非常有用,并在任何项目的生命周期中为你节省大量时间,尤其是维护部分。在非常适合系统需求的设计模式的帮助下修改和维护系统可能被证明是无价的。
为了保持文章相对简短,我们将不再展示任何示例。对于那些有兴趣的人来说,这篇文章的一大灵感来自于四本书的设计模式:可重用面向对象软件的元素和 Addy Osmani 的学习 JavaScript 设计模式。我强烈推荐这两本书。