JavaScript 设计模式核心原理与应用实践
SOLID设计原则
"SOLID" 是由罗伯特·C·马丁在 21 世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。
设计原则是设计模式的指导理论,它可以帮助我们规避不良的软件设计。SOLID 指代的五个基本原则分别是:
- 单一功能原则(Single Responsibility Principle) > 一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中,即又定义有且仅有一个原因使类变更。(甲类负责两个不同的职责:职责A,职责B。当由于职责A需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责B功能发生故障。也就是说职责A和B被耦合在了一起”)。
- 开放封闭原则(Opened Closed Principle): > 对拓展开放,对修改封闭。说得更准确点,软件实体(类、模块、函数)可以扩展,但是不可修改。
- 里式替换原则(Liskov Substitution Principle) > 一个对象在其出现的任何地方,都可以用子类实例做替换,并且不会导致程序的错误。 经典的例子: 正方形不是长方形的子类。原因是正方形多了一个属性“长 == 宽”。这时,对正 方形类设置不同的长和宽,计算面积的结果是最后设置那项的平方,而不是长*宽,从而发生了 与长方形不一致的行为。如果程序依赖了长方形的面积计算方式,并使用正方形替换了长方形, 实际表现与预期不符。
- 接口隔离原则(Interface Segregation Principle) > 接口隔离原则表明客户端不应该被强迫实现一些他们不会使用的接口,应该把胖接口中的方法分组,然后用多个接口替代它,每个接口服务于一个子模块。简单地说,就是使用多个专门的接口比使用单个接口要好很多。
- 依赖反转原则(Dependency Inversion Principle) > 抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对抽象(接口)编程,而不是针对实现细节编程。
设计模式的核心思想——封装变化
在实际开发中,不发生变化的代码可以说是不存在的。我们能做的只有将这个变化造成的影响最小化 —— 将变与不变分离,确保变化的部分灵活、不变的部分稳定。
这个过程,就叫“封装变化”;封装的正是软件中那些不稳定的要素,它是一种防患于未然的行为 —— 提前抽离了变化,就为后续的拓展提供了无限的可能性,如此,我们才能做到在变化到来的时候从容不迫。
创建型
创建型模式封装了创建对象过程中的变化
工厂模式
工厂模式其实就是将创建对象的过程单独封装。
抽象工厂模式
抽象工厂本质上处理的其实也是类,但是是一帮非常棘手、繁杂的类,这些类中不仅能划分出门派,还能划分出等级,同时存在着千变万化的扩展可能性——这使得我们必须对共性作更特别的处理、使用抽象类去降低扩展的成本,同时需要对类的性质作划分,于是有了这样的四个关键角色:
- 抽象工厂(抽象类,它不能被用于生成具体实例): 用于声明最终目标产品的共性。在一个系统里,抽象工厂可以有多个,每一个抽象工厂对应的这一类的产品,被称为“产品族”。
- 具体工厂(用于生成产品族里的一个具体的产品): 继承自抽象工厂、实现了抽象工厂里声明的那些方法,用于创建具体的产品的类。
- 抽象产品(抽象类,它不能被用于生成具体实例): 具体工厂里实现的接口,会依赖一些类,这些类对应到各种各样的具体的细粒度产品(比如操作系统、硬件等),这些具体产品类的共性各自抽离,便对应到了各自的抽象产品类。
- 具体产品(用于生成产品族里的一个具体的产品所依赖的更细粒度的产品): 比如具体的一种操作系统、或具体的一种硬件等。
单例模式
单例模式的实现思路
一般情况下,当我们创建了一个类(本质是构造函数)后,可以通过new关键字调用构造函数进而生成任意多的实例对象。
class SingleMode {
alert() {console.log("我是一个单例对象")}
}
const s1 = new SingleMode()
const s2 = new SingleMode()
s1 === s2 //false
我们先 new 了一个 s1,又 new 了一个 s2,很明显 s1 和 s2 之间没有任何瓜葛,两者是相互独立的对象,各占一块内存空间。而单例模式想要做的就是不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例。
要做到这一点,就需要构造函数具备判断自己是否已经创建过一个实例的能力。
class SingleMode {
alert() {console.log("我是一个单例对象")}
static getInstance() {
if(!SingleMode.instance) {
//如果不存在实例 ,那么先创建它
SingleMode.instance = new SingleMode()
}
//如果已经存在,直接返回
return SingleMode.instance
}
}
const s1 = new SingleMode()
const s2 = new SingleMode()
s1 === s2 //false
通过闭包实现单例模式:
SingleMode.getInstance = (function() {
// 定义自由变量instance,模拟私有变量
let instance = null
return function() {
// 判断自由变量是否为null
if(!instance) {
// 如果为null则new出唯一实例
instance = new SingleMode()
}
return instance
}
})()
可以看出,在getInstance方法的判断和拦截下,我们不管调用多少次,SingleMode都只会给我们返回一个实例,s1和s2现在都指向这个唯一的实例。
原型模式
原型模式不仅是一种设计模式,它还是一种编程范式(programming paradigm),是 JavaScript 面向对象系统实现的根基。
在原型模式下,当我们想要创建一个对象时,会先找到一个对象作为原型,然后通过克隆原型的方式来创建出一个与原型一样(共享一套数据/方法)的对象。在 JavaScript 里,Object.create
方法就是原型模式的天然实现——准确地说,只要我们还在借助Prototype
来实现对象的创建和原型的继承,那么我们就是在应用原型模式。
JavaScript 中的“类”
ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法不会为 JavaScript 引入新的面向对象的继承模型。 ——MDN 当我们尝试用 class 去定义一个 Student 类时:
class Student {
constructor(name,age,phone) {
this.name = name
this.age = age
this.phoneNum = phone
}
call() {
console.log(`${name}的电话是${this.phoneNum}`)
}
}
其实完全等价于写了这么一个构造函数:
function Student(name,age,phone) {
this.name = name
this.age = age
this.phoneNum = phone
}
Student.prototype.call = function() {
console.log(`小明的电话是${this.phoneNum}`)
}
原型编程范式的核心思想就是利用实例来描述对象,用实例作为定义对象和继承的基础。在 JavaScript 中,原型编程范式的体现就是基于原型链的继承。
所以说 JavaScript 这门语言的根本就是原型模式。在 Java 等强类型语言中,原型模式的出现是为了实现类型之间的解耦。而 JavaScript 本身类型就比较模糊,不存在类型耦合的问题,所以说咱们平时根本不会刻意地去使用原型模式。因此我们此处不必强行把原型模式当作一种设计模式去理解,把它作为一种编程范式来讨论会更合适。
结构型
结构型模式封装的是对象之间组合方式的变化,目的在于灵活地表达对象间的配合与依赖关系
装饰器模式
它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”
适配器模式
适配器模式通过把一个类的接口变换成客户端所期待的另一种接口,可以帮我们解决不兼容的问题。
好的适配器的自我修养——把变化留给自己,把统一留给用户。在此处,所有关于 http 模块、关于 xhr 的实现细节,全部被 Adapter 封装进了自己复杂的底层逻辑里,暴露给用户的都是十分简单的统一的东西——统一的接口,统一的入参,统一的出参,统一的规则。
代理模式
代理模式,式如其名——在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的,这样的模式就是代理模式。
行为型
行为型模式则将是对象千变万化的行为进行抽离,确保我们能够更安全、更方便地对行为进行更改
观察者模式
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。 —— Graphic Design Patterns
观察者模式,是所有 JavaScript 设计模式中使用频率最高,面试频率也最高的设计模式,所以说它十分重要.
观察者模式有一个“别名”,叫发布 - 订阅模式(之所以别名加了引号,是因为两者之间存在着细微的差异,下个小节里我们会讲到这点)。这个别名非常形象地诠释了观察者模式里两个核心的角色要素——“发布者”与“订阅者”。 后续文章将实现简单的发布订阅模式,并探寻vue的双向绑定的实现原理。
策略模式
策略模式的定义:
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
“封装”就是把某一功能点对应的逻辑给提出来;“可替换”建立在封装的基础上,只是说这个“替换”的判断过程,咱们不能直接怼 if-else,而要考虑更优的映射方案。
状态模式
状态模式的定义:
状态模式(State Pattern) :允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
解决的问题:
状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
迭代器模式
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。 ——《设计模式:可复用面向对象软件的基础》
迭代器模式是设计模式中少有的目的性极强的模式。所谓“目的性极强”就是说它不操心别的,它就解决这一个问题——遍历。
ES6对迭代器的实现
在“公元前”,JS原生的集合类型数据结构,只有Array(数组)和Object(对象);而ES6中,又新增了Map和Set。四种数据结构各自有着自己特别的内部实现,但我们仍期待以同样的一套规则去遍历它们,所以ES6在推出新数据结构的同时也推出了一套统一的接口机制——迭代器(Iterator)。
// 编写一个迭代器生成函数
function *iteratorGenerator() {
yield '1号选手'
yield '2号选手'
yield '3号选手'
}
const iterator = iteratorGenerator()
iterator.next() //{value:'1号选手',done: false}
iterator.next() //{value:'2号选手',done: false}
iterator.next() //{value:'3号选手',done: false}
用ES5去写一个能够生成迭代器对象的迭代器生成函数
// 定义生成器函数,入参是任意集合
function iteratorGenerator(list) {
// idx记录当前访问的索引
var idx = 0
// len记录传入集合的长度
var len = list.length
return {
// 自定义next方法
next: function() {
// 如果索引还没有超出集合长度,done为false
var done = idx >= len
// 如果done为false,则可以继续取值
var value = !done ? list[idx++] : undefined
// 将当前值与遍历是否完毕(done)返回
return {
done: done,
value: value
}
}
}
}
var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手'])
iterator.next()
iterator.next()
iterator.next()
参考: 掘金小册《JavaScript 设计模式核心原理与应⽤实践》