JavaScript 设计模式综合指南

已发表: 2022-03-11

作为一名优秀的 JavaScript 开发人员,您努力编写干净、健康和可维护的代码。 您解决的有趣挑战虽然独特,但不一定需要独特的解决方案。 您可能已经发现自己编写的代码看起来与您之前处理过的完全不同的问题的解决方案相似。 您可能不知道,但您使用过 JavaScript设计模式。 设计模式是软件设计中常见问题的可重用解决方案。

JavaScript 设计模式综合指南

在任何语言的生命周期中,许多此类可重用的解决方案都是由该语言社区的大量开发人员制作和测试的。 正是由于许多开发人员的这种综合经验,这些解决方案非常有用,因为它们可以帮助我们以优化的方式编写代码,同时解决手头的问题。

我们从设计模式中获得的主要好处如下:

  • 它们是经过验证的解决方案:因为许多开发人员经常使用设计模式,所以您可以确定它们是有效的。 不仅如此,您还可以确定它们已被多次修改,并且可能已经实施了优化。
  • 它们易于重用:设计模式记录了可重用的解决方案,可以对其进行修改以解决多个特定问题,因为它们与特定问题无关。
  • 它们富有表现力:设计模式可以非常优雅地解释大型解决方案。
  • 他们简化了沟通:当开发人员熟悉设计模式时,他们可以更轻松地就给定问题的潜在解决方案相互沟通。
  • 它们避免了重构代码的需要:如果在编写应用程序时考虑到了设计模式,通常情况下您以后不需要重构代码,因为将正确的设计模式应用于给定的问题已经是最优的了解决方案。
  • 它们降低了代码库的大小:因为设计模式通常是优雅和最佳的解决方案,它们通常比其他解决方案需要更少的代码。

我知道你现在已经准备好开始了,但在你学习所有关于设计模式的知识之前,让我们回顾一些 JavaScript 基础知识。

JavaScript 简史

JavaScript 是当今最流行的 Web 开发编程语言之一。 它最初是作为各种显示的 HTML 元素(称为客户端脚本语言)的一种“粘合剂”,用于最初的 Web 浏览器之一。 名为 Netscape Navigator,当时只能显示静态 HTML。 正如您可能假设的那样,这种脚本语言的想法导致了当时浏览器开发行业的大玩家之间的浏览器战争,例如 Netscape Communications(今天的 Mozilla)、微软和其他公司。

每个大玩家都想推动他们自己实现这种脚本语言,所以 Netscape 制作了 JavaScript(实际上,Brendan Eich 做了),微软制作了 JScript,等等。 如您所见,这些实现之间的差异很大,因此 Web 浏览器的开发是针对每个浏览器进行的,并带有网页附带的最佳查看贴纸。 很快就清楚我们需要一个标准的、跨浏览器的解决方案来统一开发过程并简化网页的创建。 他们提出的称为 ECMAScript。

ECMAScript 是所有现代浏览器都试图支持的标准化脚本语言规范,并且 ECMAScript 有多种实现(可以说是方言)。 最受欢迎的是本文的主题,JavaScript。 自最初发布以来,ECMAScript 已经标准化了许多重要的事情,对于那些对细节更感兴趣的人,维基百科上提供了每个版本的 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 将函数视为一等公民)发送给另一个函数并在事件触发后执行的函数。 这通常用于订阅事件,例如鼠标单击或键盘按钮按下。

JavaScript 事件循环的图形描述

每次带有监听器的事件触发(否则事件丢失)时,一条消息被发送到正在同步处理的消息队列中,以 FIFO 方式(先进先出)。 这称为事件循环

队列中的每条消息都有一个与之关联的函数。 一旦消息出队,运行时会在处理任何其他消息之前完全执行该函数。 也就是说,如果一个函数包含其他函数调用,它们都是在处理队列中的新消息之前执行的。 这称为运行至完成。

 while (queue.waitForMessage()) { queue.processNextMessage(); }

queue.waitForMessage()同步等待新消息。 每个正在处理的消息都有自己的堆栈,并且一直处理到堆栈为空。 完成后,将从队列中处理一条新消息(如果有的话)。

您可能还听说过 JavaScript 是非阻塞的,这意味着当执行异步操作时,程序可以在等待异步操作完成的同时处理其他事情,例如接收用户输入,而不是阻塞 main执行线程。 这是 JavaScript 的一个非常有用的属性,整篇文章都可以写在这个主题上; 但是,它超出了本文的范围。

什么是设计模式?

正如我之前所说,设计模式是软件设计中常见问题的可重用解决方案。 让我们看一下设计模式的一些类别。

原型模式

如何创建一个模式? 假设您发现了一个常见问题,并且您有自己独特的解决方案来解决这个问题,但该解决方案并未在全球范围内得到认可和记录。 每次遇到此问题时,您都会使用此解决方案,并且您认为它是可重用的,并且开发人员社区可以从中受益。

它是否立即成为一种模式? 幸运的是,没有。 很多时候,一个人可能有很好的代码编写习惯,只是把看起来像模式的东西误认为是一种模式,而实际上它不是一种模式。

你怎么知道你认为你认识的实际上是一种设计模式?

通过了解其他开发人员的意见,了解创建模式本身的过程,并让自己熟悉现有模式。 模式在成为成熟模式之前必须经过一个阶段,这称为原型模式。

如果原型模式通过了各种开发人员和场景的一定时期的测试,并且该模式被证明是有用的并给出正确的结果,那么它就是一个未来的模式。 有相当多的工作和文档——其中大部分超出了本文的范围——需要完成,以形成一个被社区认可的成熟模式。

反模式

设计模式代表了良好的实践,反模式则代表了不好的实践。

反模式的一个例子是修改Object类原型。 JavaScript 中几乎所有的对象都继承自Object (请记住 JavaScript 使用基于原型的继承),因此想象一下您更改了此原型的场景。 从这个原型继承的所有对象中都可以看到对Object原型的更改——这将是大多数JavaScript 对象。 这是一场等待发生的灾难。

另一个与上述类似的示例是修改您不拥有的对象。 这方面的一个例子是覆盖整个应用程序中许多场景中使用的对象的函数。 如果你和一个大团队一起工作,想象一下这会造成什么混乱; 您很快就会遇到命名冲突、不兼容的实现和维护噩梦。

与了解所有好的做法和解决方案的用处类似,了解不好的做法和解决方案也非常重要。 这样,您可以识别它们并避免预先犯错误。

设计模式分类

设计模式可以按多种方式分类,但最流行的一种是:

  • 创造型设计模式
  • 结构设计模式
  • 行为设计模式
  • 并发设计模式
  • 架构设计模式

创意设计模式

这些模式处理与基本方法相比优化对象创建的对象创建机制。 对象创建的基本形式可能会导致设计问题或增加设计的复杂性。 创建型设计模式通过某种方式控制对象创建来解决这个问题。 此类别中一些流行的设计模式是:

  • 工厂方法
  • 抽象工厂
  • 建造者
  • 原型
  • 辛格尔顿

结构设计模式

这些模式处理对象关系。 他们确保如果系统的某个部分发生变化,整个系统不需要随之变化。 此类别中最流行的模式是:

  • 适配器
  • 合成的
  • 装饰器
  • 正面
  • 蝇量级
  • 代理

行为设计模式

这些类型的模式识别、实现和改进系统中不同对象之间的通信。 它们有助于确保系统的不同部分具有同步信息。 这些模式的流行示例是:

  • 责任链
  • 命令
  • 迭代器
  • 调解员
  • 纪念
  • 观察者
  • 状态
  • 战略
  • 游客

并发设计模式

这些类型的设计模式处理多线程编程范例。 一些流行的是:

  • 活动对象
  • 核反应
  • 调度器

建筑设计模式

用于架构目的的设计模式。 一些最著名的是:

  • 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 使用基于原型的继承。 前一种方法的问题是方法writesCodePerson构造函数的每个实例重新定义。 我们可以通过将方法设置到函数原型中来避免这种情况:

 // 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,我们将 counter 变量绑定到一个函数,该函数被调用并关闭,但仍可以被递增它的子函数访问。 由于我们无法从函数表达式外部访问计数器变量,因此我们通过范围操作将其设为私有。

使用闭包,我们可以创建具有私有和公共部分的对象。 这些被称为模块,当我们想要隐藏对象的某些部分并且只向模块的用户公开接口时,它们非常有用。 让我们在一个例子中展示这一点:

 // 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 对象作为原型。 有问题的情况如下:

  1. 如果我们有一个引用公共函数的私有函数,我们不能覆盖公共函数,因为私有函数将继续引用该函数的私有实现,从而在我们的系统中引入一个错误。
  2. 如果我们有一个公共成员指向一个私有变量,并试图从模块外部覆盖公共成员,其他函数仍然会引用该变量的私有值,从而在我们的系统中引入一个错误。

单例模式

当我们只需要一个类的一个实例时,就会使用单例模式。 例如,我们需要一个包含某些配置的对象。 在这些情况下,只要系统中某处需要配置对象,就不必创建新对象。

 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();

Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.

命令模式

The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.

This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.

As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.

 // 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));

立面图案

The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.

A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:

 jQuery(".parent .child div.span")

It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.

We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.

下一步

Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.

In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.

相关:作为一名 JS 开发人员,这就是让我彻夜难眠的原因 / 理解 ES6 类的混乱