作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Marko Mišura所言的头像

Marko Mišura所言

做过全栈软件工程师, 马尔科是设计方面的专家, 网络系统的实施和维护.

专业知识

工作经验

10

分享

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

JavaScript设计模式综合指南

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

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

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

我知道你已经准备好加入了, 但是在你学习JavaScript中的设计模式之前, 让我们回顾一些基础知识.

JavaScript简史

JavaScript是当今web开发中最流行的编程语言之一. 它最初是作为各种显示的HTML元素的“粘合剂”, 被称为客户端脚本语言, 对于最初的web浏览器之一. 它被称为Netscape Navigator,当时只能显示静态HTML. 你可能会想, 这种脚本语言的想法导致了当时浏览器开发行业巨头之间的浏览器大战, 例如Netscape Communications(今天的Mozilla), 微软, 和其他人.

每个大公司都想推出自己的脚本语言实现, 所以网景开发了JavaScript, Brendan Eich说过), 微软制作的JScript, 诸如此类. 你们可以想象, 这些实现之间的差异很大, 所以网页浏览器的开发是针对每个浏览器的, 带着点击量最高的贴纸,还有一个网页. 我们很快意识到我们需要一个标准, 一个跨浏览器的解决方案,统一开发过程,简化网页的创建. 他们想出的东西叫做 ECMAScript.

ECMAScript是一种标准化的脚本语言规范,所有现代浏览器都试图支持它, ECMAScript有多种实现(可以说是方言). 其中最流行的是本文的主题JavaScript. 自最初发布以来, ECMAScript标准化了很多重要的东西, 对于那些对细节更感兴趣的人, Wikipedia上提供了每个ECMAScript版本的标准化项目的详细列表. 浏览器对ECMAScript版本6 (ES6)及更高版本的支持仍然不完整,必须编译到ES5才能得到完全支持.

什么是JavaScript?

为了全面把握本文的内容, 在深入了解JavaScript模式之前,让我们先介绍一些非常重要的语言特性. 如果有人问你“什么是JavaScript?你可能会这样回答:

JavaScript是轻量级的, 解释, 具有一等函数的面向对象编程语言,通常被称为网页脚本语言.

上述定义意味着JavaScript代码具有较低的内存占用, 很容易实现, 而且很容易学, 其语法类似于c++和Java等流行语言. 它是一种脚本语言,这意味着它的代码是解释的,而不是编译的. 它支持程序, 面向对象的, 函数式编程风格, 这使得它对开发人员来说非常灵活.

到目前为止, 我们已经研究了所有听起来像其他语言的特征, 那么让我们来看看JavaScript相对于其他语言有什么特殊之处. 我将列出一些特征,并尽我所能解释为什么它们值得特别关注.

JavaScript支持一等函数

当我刚开始使用JavaScript时,这个特性对我来说很难掌握, 因为我的背景是C/ c++. JavaScript将函数视为一等公民, 这意味着你可以把函数作为参数传递给其他函数就像传递其他变量一样.

//我们将函数作为参数发送给be
//从调用函数内部执行
function performOperation(a, b, cb) {
    Var c = a + b;
    cb (c);
}

performOperation(2,3, function(result) {
    //输出5
    控制台.log("操作的结果是" + result);
})

JavaScript是基于原型的

这是许多其他面向对象语言的情况, JavaScript支持对象, 当我们想到对象时首先想到的一个术语就是类和继承. 这就有点棘手了, 因为该语言不支持普通语言形式的类,而是使用基于原型或基于实例的继承.

只是现在,在ES6中,正式的术语 class 介绍了, 这意味着浏览器仍然不支持这个(如果你还记得吗, 在撰写本文时, 最后一个完全支持的ECMAScript版本是5.1). 值得注意的是, 然而, 即使术语“类”被引入到JavaScript中, 它仍然在底层使用基于原型的继承.

基于原型的编程是一种面向对象编程风格,其中行为重用(称为继承)是通过作为原型的委托重用现有对象的过程来执行的. 一旦我们进入本文的设计模式部分,我们将对此进行更详细的研究, 因为这个特性在很多JS设计模式中都有使用.

JavaScript事件循环

如果您有使用JavaScript的经验,您肯定对这个术语很熟悉 回调函数. 给那些不熟悉这个词的人, 回调函数是作为参数发送的函数(记住, JavaScript将函数视为另一个函数的一等公民,并在事件触发后执行. 这通常用于订阅诸如鼠标单击或键盘按钮按下之类的事件.

JavaScript事件循环的图形描述

每次一个事件, 它有一个附加的监听器, 触发(否则事件丢失), 消息被发送到同步处理的消息队列, 以先进先出的方式. 这叫做 事件循环.

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

而(队列.waitForMessage ()) {
    队列.processNextMessage ();
}

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

您可能还听说过JavaScript是非阻塞的,这意味着当 异步操作 正在执行, 这个程序还能处理其他事情, 例如接收用户输入, 在等待异步操作完成时, 不阻塞主执行线程. This is a very useful property of JavaScript and a whole article could be written just on 这 topic; 然而, 这超出了本文的讨论范围.

什么是JavaScript中的设计模式?

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

Proto-模式

如何创建一个模式? 假设您发现了一个常见的问题, 对于这个问题,你有自己独特的解决方案, 哪些没有得到全球认可和记录. 每次遇到这个问题都使用这个解决方案, 你认为它是可重用的,开发者社区可以从中受益.

它会立即成为一种模式吗? 幸运的是,没有. 通常, 一个人可能有良好的代码编写实践,只是把一些看起来像模式的东西误认为是一个模式, 事实上, 这不是一种模式.

您如何知道您认为您识别的实际上是一个设计模式?

通过征求其他开发者的意见, 通过了解创建模式本身的过程, 让自己熟悉现有的模式. 在一个模式成为一个成熟的模式之前,它必须经历一个阶段, 这被称为原型模式.

原型模式是未来的模式 if 它通过了由各种开发人员和场景进行的一段时间的测试,在这些测试中,模式被证明是有用的,并给出了正确的结果. 要创建一个被社区认可的成熟模式,需要完成大量的工作和文档(其中大部分超出了本文的范围).

反模式

设计模式代表好的实践,反模式代表坏的实践.

反模式的一个例子是修改 Object 类的原型. JavaScript中几乎所有的对象都继承自 Object (请记住,JavaScript使用基于原型的继承),所以想象一下您修改了这个原型的场景. 的更改 Object 原型会出现在所有从这个原型继承的对象中也就是 大多数 JavaScript对象. 这是一场即将发生的灾难.

另一个类似于上面提到的例子是修改不属于您的对象. 这方面的一个例子是覆盖在整个应用程序的许多场景中使用的对象的函数. 如果你和一个大团队一起工作, imagine the confusion 这 would cause; you’d quickly run into naming collisions, 不兼容的实现, 还有维修噩梦.

这与了解所有好的实践和解决方案的用处类似, 了解坏的也很重要. 这样,你就可以识别它们,避免一开始就犯错误.

设计模式分类

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

  • 创造性 设计模式
  • 结构 设计模式
  • 行为 设计模式
  • 并发性 设计模式
  • 建筑 设计 模式

创意设计模式

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

  • 工厂方法
  • 抽象工厂
  • 构建器
  • 原型
  • 单例

结构设计模式

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

  • 适配器
  • 复合
  • 装饰
  • 外观
  • 轻量级选手
  • 代理

行为设计模式

这些类型的模式识别, 实现, 并改善系统中不同对象之间的通信. 它们有助于确保系统的不同部分具有同步的信息. 这些模式的常见示例有:

  • 责任链
  • Command
  • 迭代器
  • 中介
  • 纪念品
  • 观察者
  • 状态
  • 策略
  • 游客

并发设计模式

这些类型的设计模式处理多线程编程范例. 一些受欢迎的是:

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

建筑设计模式

用于架构目的的设计模式. 其中最著名的有:

  • MVC(模型-视图-控制器)
  • MVP (Model-View-Presenter)
  • MVVM (Model-View-ViewModel)

在下一节中, 为了更好地理解,我们将通过提供示例来仔细研究前面提到的一些设计模式.

设计模式示例

每个设计模式都代表针对特定类型问题的特定类型的解决方案. 没有一套通用的模式总是最适合的. 我们需要了解一个特定的模式何时会被证明是有用的,以及它是否会提供实际价值. 一旦我们熟悉了它们最适合的模式和场景, 我们可以很容易地确定特定模式是否适合给定的问题.

还记得, 对给定的问题应用错误的模式可能会导致不必要的影响,例如不必要的代码复杂性, 不必要的性能开销, 甚至是生成一个新的反模式.

在考虑将设计模式应用于代码时,这些都是需要考虑的重要事项. 我们将看一下JS中的一些设计模式,这些模式是每个高级JavaScript开发人员都应该熟悉的.

构造函数模式

在考虑经典的面向对象语言时, 构造函数是类中的一个特殊函数,它用一组默认值和/或传入值初始化对象.

在JavaScript中创建对象的常用方法有以下三种:

//可以使用以下两种方法创建一个新对象
Var 实例 = {};
// or
var 实例 =对象.创建(对象.原型);
// or
var 实例 = new Object();

创建对象之后,有四种方法(从ES3开始)可以向这些对象添加属性. 它们是:

// ES3开始支持
//点表示法
实例.key = "键的值";

//方括号符号
实例["key"] = "key的值";

//从ES5开始支持
//使用Object设置单个属性.defineProperty
Object.定义属性(实例,"key", {)
    value:“键值”,
    可写:没错,
    可列举的:真的,
    可配置:真
});

//使用Object设置多个属性.defineProperties
Object.defineProperties(例如,{
    " firstKey ": {
        value:“第一个键的值”,
        可写:真
    },
    " secondKey ": {
        value:“第二个键的值”,
        可写:假
    }
});

创建对象最常用的方法是使用大括号和, 用于添加属性, 点符号或方括号. 任何有JavaScript经验的人都使用过它们.

我们前面提到JavaScript不支持本机类, 但是它通过在函数调用前使用“new”关键字来支持构造函数. 这种方式, 可以将该函数用作构造函数,并像使用经典语言构造函数一样初始化其属性.

//我们为人对象定义一个构造函数
函数人(姓名,年龄,isDeveloper) {
    这.Name = Name;
    这.年龄=年龄;
    这.isDeveloper = isDeveloper || false;

    这.writesCode = function() {
      控制台.日志(这.isDeveloper? “这个人确实写代码”:“这个人不写代码”);
    }
}

//创建一个人实例,属性名为:Bob, 年龄:38, isDeveloper: true和writesCode方法
var person1 = new 人(“Bob”,38,true);
//创建一个人实例,属性名为:Alice, 年龄:32, isDeveloper: false和writesCode方法
var person2 = new 人("Alice", 32);

//打印出:这个人确实写代码
person1.writesCode ();
//输出:这个人不写代码
person2.writesCode ();

然而,这里仍有改进的余地. 如果您还记得,我在前面提到过JavaScript使用基于原型的继承. 前一种方法的问题在于 writesCode 类的每个实例重新定义 构造函数. 我们可以通过在函数原型中设置方法来避免这种情况:

//我们为人对象定义一个构造函数
函数人(姓名,年龄,isDeveloper) {
    这.Name = Name;
    这.年龄=年龄;
    这.isDeveloper = isDeveloper || false;
}

//我们扩展函数的原型
人.原型.writesCode = function() {
    控制台.日志(这.isDeveloper? “这个人确实写代码”:“这个人不写代码”);
}

//创建一个人实例,属性名为:Bob, 年龄:38, isDeveloper: true和writesCode方法
var person1 = new 人(“Bob”,38,true);
//创建一个人实例,属性名为:Alice, 年龄:32, isDeveloper: false和writesCode方法
var person2 = new 人("Alice", 32);

//打印出:这个人确实写代码
person1.writesCode ();
//输出:这个人不写代码
person2.writesCode ();

现在,两个例子 类的共享实例 writesCode () 方法.

模块的模式

就特性而言,JavaScript总是令人惊叹不已. JavaScript的另一个特殊之处(至少就面向对象语言而言)是JavaScript不支持访问修饰符. 在经典的OOP语言中,用户定义一个类并确定其成员的访问权限. 因为纯形式的JavaScript既不支持类也不支持访问修饰符, JavaScript开发人员找到了一种在需要时模拟这种行为的方法.

在讨论模块模式细节之前,让我们先讨论一下闭包的概念. A 关闭 函数是否具有访问父作用域的权限,即使在父函数关闭之后. 它们通过作用域帮助我们模仿访问修饰符的行为. 让我们通过一个例子来说明这一点:

//我们使用了立即调用的函数表达式
//创建私有变量counter
var counterIncrementer = (function() {
    Var counter = 0;

    返回函数(){
        返回+ +计数器;
    };
})();

//输出1
控制台.日志(counterIncrementer ());
//输出2
控制台.日志(counterIncrementer ());
//输出3
控制台.日志(counterIncrementer ());

如你所见, 通过使用IIFE, 我们将计数器变量绑定到一个函数,该函数被调用并关闭,但仍然可以被对其进行加1的子函数访问. 因为我们不能从函数表达式外部访问计数器变量, 我们通过作用域操作使其私有.

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

//通过使用闭包,我们公开了一个对象
//作为管理私有对象数组的公共API
Var 集合 = (function() {
    //私有成员
    Var 对象 = [];

    //公共成员
    返回{
        addObject:函数(对象){
            对象.推动(对象);
        },
        removeObject:函数(对象){
            Var index = 对象.indexOf(对象);
            if (index >= 0) {
                对象.拼接(指数(1);
            }
        },
        ge对象: function() {
            返回JSON.解析(JSON.stringify(对象));
        }
    };
})();

集合.addObject(“Bob”);
集合.addObject(“爱丽丝”);
集合.addObject(“弗兰克”);
//打印["Bob", "Alice", "Franck"]
控制台.日志(集合.getobject ());
集合.removeObject(“爱丽丝”);
//打印["Bob", "Franck"]
控制台.日志(集合.getobject ());

这种模式引入的最有用的东西是对象的私有部分和公共部分的明确分离, 哪个概念与来自经典面向对象背景的开发人员非常相似.

然而,并不是一切都那么完美. 当您希望更改成员的可见性时, 由于访问公共部分和私有部分的性质不同,您需要在使用该成员的任何地方修改代码. 也, 创建后添加到对象的方法不能访问对象的私有成员.

揭示模块模式

此模式是对上面所示的模块模式的改进. 主要区别在于,我们在模块的私有范围内编写整个对象逻辑,然后通过返回一个匿名对象,简单地公开我们希望成为公共的部分. 在将私有成员映射到其对应的公共成员时,还可以更改私有成员的命名.

//我们将整个对象逻辑写成私有成员和
//公开一个匿名对象,该对象映射我们希望显示的成员
//到对应的public成员
var namesCollection = (function() {
    //私有成员
    Var 对象 = [];

    addObject(object) {
        对象.推动(对象);
    }

    函数removeObject(object) {
        Var index = 对象.indexOf(对象);
        if (index >= 0) {
            对象.拼接(指数(1);
        }
    }

    函数ge对象 () {
        返回JSON.解析(JSON.stringify(对象));
    }

    //公共成员
    返回{
        addName: addObject,
        removeName: removeObject,
        getname: getobject
    };
})();

namesCollection.addName(“Bob”);
namesCollection.addName(“爱丽丝”);
namesCollection.addName(“弗兰克”);
//打印["Bob", "Alice", "Franck"]
控制台.日志(namesCollection.getname ());
namesCollection.removeName(“爱丽丝”);
//打印["Bob", "Franck"]
控制台.日志(namesCollection.getname ());

揭示模块模式是我们实现模块模式的至少三种方式之一. 揭示模块模式与模块模式的其他变体之间的区别主要在于如何引用公共成员. 结果是, 揭示模块模式 is much easier to use and modify; 然而, 在某些情况下,它可能是脆弱的, 比如在继承链中使用RMP对象作为原型. 有问题的情况如下:

  1. 如果我们有一个私有函数,它引用一个公共函数, 我们不能推翻公共职能, 由于私有函数将继续引用函数的私有实现, 这样就给我们的系统引入了一个bug.
  2. 如果有一个公共成员指向一个私有变量, 并尝试从模块外部重写公共成员, 其他函数仍然会引用变量的私有值, 在我们的系统中引入一个bug.

单例模式

当我们只需要一个类的实例时,使用单例模式. 例如,我们需要有一个对象,其中包含一些配置的东西. 在这些情况下, 当系统中的某个地方需要配置对象时,不需要创建新对象.

Var singleton = (function() {
    //只初始化一次的私有单例值
    var配置;

    函数initializeConfiguration(值){
        这.randomNumber =数学.随机();
        Values = Values || {};
        这.数字=值.编号|| 5;
        这.Size = values.尺寸|| 10;
    }

    //导出检索单例值的集中式方法
    返回{
        getConfig:函数(值){
            //我们只初始化单例值一次
            如果(config === undefined) {
                config = new initializeConfiguration(values);
            }

            //并在请求的地方返回相同的配置值
            返回配置;
        }
    };
})();

var configObject = singleton.getConfig({"size": 8});
//打印数字:5,大小:8,随机数字:someRandomDecimalValue
控制台.日志(configObject);
var configObject1 = singleton.getConfig({"number": 8});
//打印数字:5,大小:8,randomNumber:与第一次配置相同的randomDecimalValue
控制台.日志(configObject1);

正如您在示例中看到的那样, 生成的随机数总是相同的, 以及发送进来的配置值.

重要的是要注意,检索单例值的访问点必须只有一个并且是众所周知的. 使用这种模式的缺点是测试起来相当困难.

观察者模式

当我们需要以优化的方式改进系统中不同部分之间的通信时,观察者模式是一个非常有用的工具. 它促进了对象之间的松耦合.

这个模式有不同的版本, 但这是最基本的形式, 这个模式有两个主要部分. 前者是主体,后者是观察者.

主题处理与观察者订阅的某个主题相关的所有操作. 这些操作将观察者订阅到某个主题, 取消订阅观察者的某个主题, 并在发布事件时通知观察者某个主题.

然而, 此模式有一种变体,称为发布者/订阅者模式, 我将在本节中使用它作为一个例子. 经典观察者模式和发布者/订阅者模式之间的主要区别在于,发布者/订阅者模式比观察者模式促进了更加松散的耦合.

在观察者模式中, 主题保存对订阅的观察者的引用,并直接从对象本身调用方法,而, 在发布者/订阅者模式中, 我们有频道, 哪些作为订阅者和发布者之间的通信桥梁. 发布者触发一个事件,并简单地执行为该事件发送的回调函数.

我将展示一个发布者/订阅者模式的简短示例, 但是对于那些感兴趣的人, 一个经典的观察者模式示例可以很容易地在网上找到.

var publisherSubscriber = {};

//我们发送一个容器对象来处理订阅和发布
(函数(容器){
    //该id表示一个主题的唯一订阅id
    Var id = 0;

    //我们通过发送订阅一个特定的主题
    //事件触发时执行的回调函数
    容器.订阅=函数(主题,f) {
        if (!(容器中的主题)){
          容器[topic] = [];
        }

        容器(主题).({推
            “id”:+ +身份证,
            “回调”:f
        });

        返回id;
    }

    //每个订阅都有自己的唯一ID,我们使用它
    //从某个主题中删除订阅者
    容器.取消订阅= function(topic, id) {
        Var订户= [];
        对于(var订阅者的容器[主题]){
            如果用户.id !== id) {
                用户.推动(订户);
            }
        }
        容器[topic] =订阅者;
    }

    容器.Publish = function(topic, data) {
        对于(var订阅者的容器[主题]){
            //当执行回调时,通常读取
            //在文档中知道哪些参数将是
            //由触发事件的对象传递给回调函数
            订阅者.回调(数据);
        }
    }

}) (publisherSubscriber);

var subscriptionID1 = publisherSubscriber.订阅(" mouseclick ", function(data) {
    控制台.“我是Bob的鼠标点击事件的回调函数,这是我的事件数据:”+ JSON.stringify(数据));
});

var subscriptionID2 = publisherSubscriber.subscribe(" mousehover ", function(data) {
    控制台.“我是Bob的鼠标悬停事件回调函数,这是我的事件数据:”+ JSON.stringify(数据));
});

var subscriptionID3 = publisherSubscriber.订阅(" mouseclick ", function(data) {
    控制台.“我是Alice的回调函数,用于鼠标点击事件,这是我的事件数据:”+ JSON.stringify(数据));
});

//注意:在发布一个事件和它的数据之后,所有的
//订阅的回调函数将执行并接收
//触发事件的对象的数据对象
//有3个控制台.执行日志
publisherSubscriber.publish(" mouseclick ", {"data": "data1"});
publisherSubscriber.publish(" mousehover ", {"data": "data2"});

//通过移除订阅ID来取消订阅事件
publisherSubscriber.退订(“mouseClicked”,subscriptionID3);

//有2个控制台.执行日志
publisherSubscriber.publish(" mouseclick ", {"data": "data1"});
publisherSubscriber.publish(" mousehover ", {"data": "data2"});

当我们需要对触发的单个事件执行多个操作时,此设计模式非常有用. 假设您有一个场景,我们需要对后端服务进行多个AJAX调用,然后根据结果执行其他AJAX调用. 您必须将AJAX调用一个嵌套到另一个中, 可能会进入所谓的回调地狱. 使用发布者/订阅者模式是一种优雅得多的解决方案.

使用这种模式的缺点是难以测试系统的各个部分. 对于我们来说,没有一种优雅的方法可以知道系统的订阅部分是否按照预期的方式运行.

调停者模式

我们将简要介绍一个模式,它在讨论解耦系统时也非常有用. 当我们有一个场景,一个系统的多个部分需要通信和协调, 也许一个好的解决办法是引入一个调解人.

中介是一个对象,它被用作系统不同部分之间通信的中心点,并处理它们之间的工作流. 现在,重要的是要强调它处理工作流. 为什么这很重要??

因为这与发布者/订阅者模式非常相似. 你可能会问自己, OK, 所以这两种模式都有助于实现对象之间更好的通信…区别是什么?

不同之处在于中介处理工作流, 而发布者/订阅者使用一种叫做“即发即忘”的通信方式. 发布者/订阅者只是一个事件聚合器, 这意味着它只负责触发事件,并让正确的订阅者知道触发了哪些事件. 事件聚合器并不关心事件被触发后发生了什么, 调解员不是这种情况吗.

中介的一个很好的例子是向导类型的接口. 假设您有一个大型的系统注册过程. 通常, 当需要用户提供大量信息时, 将其分解为多个步骤是一个很好的实践.

这种方式, 代码将更干净(更容易维护),用户不会被仅仅为了完成注册而请求的信息量所淹没. 中介是处理注册步骤的对象, 考虑到可能发生的不同工作流程,因为每个用户可能有一个唯一的注册过程.

这种设计模式的明显好处是改进了系统不同部分之间的通信, 现在它们都通过中介器和更干净的代码库进行通信.

一个缺点是,现在我们在系统中引入了单点故障, 意思是如果我们的调解人失败了, 整个系统可能会停止工作.

原型模式

正如我们在整篇文章中已经提到的, JavaScript不支持原生形式的类. 对象之间的继承是使用基于原型的编程实现的.

它使我们能够创建对象,这些对象可以作为正在创建的其他对象的原型. 原型对象被用作构造函数创建的每个对象的蓝图.

正如我们在前几节中已经讨论过的那样, 让我们通过一个简单的示例来展示如何使用此模式.

var person原型 = {
    sayHi:函数(){
        控制台.log(“你好,我的名字是”+ 这.name + ", and I am " + 这.年龄);
    },
    sayBye:函数(){
        控制台.日志(“再见!");
    }
};

函数人(姓名,年龄){
    name = name || "John Doe";
    年龄=年龄|| 26;

    函数构造函数(名字,年龄){
        这.Name = Name;
        这.年龄=年龄;
    };

    构造函数Function.原型 = person原型;

    var 实例 = new 构造函数Function(name, 年龄);
    返回实例;
}

var person1 = 人();
var person2 = 人("Bob", 38);

//输出Hello, my name is John Doe,今年26岁
person1.sayHi ();
//输出Hello, my name is Bob, and my 38
person2.sayHi ();

请注意原型继承是如何提高性能的,因为两个对象都包含对原型本身中实现的函数的引用, 而不是在每个对象中.

命令模式

当我们想要将执行命令的对象与发出命令的对象解耦时,命令模式非常有用. 例如,假设我们的应用程序使用大量API服务调用的场景. 然后,假设API服务发生了变化. 我们必须在调用更改的api的任何地方修改代码.

这将是实现抽象层的好地方, 将调用API服务的对象与告诉它们的对象分开 调用API服务. 这种方式, 我们避免在需要调用服务的所有地方进行修改, 而是只需要改变调用本身的对象, 哪一个只有一个地方.

与任何其他模式一样,我们必须确切地知道何时真正需要这种模式. 我们需要意识到我们正在做的权衡, 因为我们在API调用上添加了一个额外的抽象层, 这将降低性能,但当我们需要修改执行命令的对象时,可能会节省大量时间.

//知道如何执行命令的对象
Var invoker = {
    添加:function(x, y) {
        返回x + y;
    },
    减去:function(x, y) {
        返回x - y;
    }
}

//被用作抽象层的对象
// executing commands; it represents an interface
//指向调用程序对象
Var manager = {
    执行:函数(name, args) {
        如果(调用器中的名称){
            返回调用程序(名字).应用(调用程序,[].片.调用(参数,1));
        }
        返回错误;
    }
}

//打印8
控制台.日志(经理.Execute ("add", 3,5));
//打印2
控制台.日志(经理.Execute ("subtract", 5, 3));

门面模式

当我们想要在公开显示的内容和幕后实现的内容之间创建抽象层时,使用facade模式. 当需要一个更容易或更简单的底层对象接口时,使用它.

这种模式的一个很好的例子是来自DOM操作库(如jQuery)的选择器, Dojo, 或D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:

jQuery(“.家长。 .孩子div.跨度”)

它大大简化了选择功能, 尽管表面上看起来很简单, 为了使其工作,在引擎盖下实现了一个完整的复杂逻辑.

我们还需要意识到性能简单性的权衡. 如果没有足够的好处,最好避免额外的复杂性. 在上述库的情况下, 这种权衡是值得的, 因为它们都是非常成功的图书馆.

下一个步骤

设计模式是一个非常有用的工具 高级JavaScript开发人员 应该意识到. 了解有关设计模式的细节可以证明非常有用,并在任何项目的生命周期中节省大量时间, 尤其是维修部分. 修改和维护在设计模式的帮助下编写的系统是非常适合系统需求的,这是非常宝贵的.

为了使文章相对简短,我们将不再展示任何示例. 对于那些感兴趣的人,这篇文章的灵感来自四人帮的书 设计模式:可重用的面向对象软件的元素 和Addy Osmani的 学习JavaScript设计模式. 我强烈推荐这两本书.

了解基本知识

  • JavaScript的主要特点是什么?

    JavaScript是异步的,支持一级函数,并且是基于原型的.

  • 什么是设计模式,它们如何支持软件架构师?

    设计模式是软件设计中常见问题的可重用解决方案. 它们是经过验证的解决方案,易于重用和表达. 它们降低了代码库的大小, 防止将来的重构, 并使您的代码更容易被其他开发人员理解.

  • 什么是JavaScript ?它是如何诞生的?

    JavaScript是浏览器的客户端脚本语言,最初是由Brendan Eich为Netscape Navigator(即现在的Mozilla)创建的.

  • 什么是ECMAScript?

    ECMAScript是一种标准化的脚本语言规范,所有现代浏览器都试图支持它. ECMAScript有多种实现,其中最流行的是JavaScript.

  • 什么是原型模式?

    如果原型模式通过了由各种开发人员和场景进行的一段时间的测试,并且该模式被证明是有用的并给出了正确的结果,那么原型模式就是未来的模式.

  • 什么是反模式?

    如果设计模式代表好的实践,那么反模式就代表坏的实践. 反模式的一个例子是修改Object类原型. 对象原型的更改可以在从该原型继承的所有对象中看到(即, JavaScript中几乎所有的对象).

  • 我们如何对设计模式进行分类?

    设计模式可以是创建的、结构的、行为的、并发的或架构的.

  • 设计模式的几个例子是什么?

    上面文章中讨论的一些示例包括构造函数模式, 模块模式, 揭示模块模式, 单例模式, 观察者模式, 中介模式, 原型模式, 命令模式, 还有立面图案.

就这一主题咨询作者或专家.
预约电话
Marko Mišura所言的头像
Marko Mišura所言

位于 克罗地亚的萨格勒布

成员自 2016年6月19日

作者简介

做过全栈软件工程师, 马尔科是设计方面的专家, 网络系统的实施和维护.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

工作经验

10

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.