JavaScript设计模式之基础知识

1,877 阅读10分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

背景

在平时的工作中,大家经常会遇到各种需求,比如最常见的:登录注册、消息通知、用户权限等等。这些功能相信大家都可以很容易的实现。但最痛苦的通常是修改代码,大部分情况下,为了保证系统的稳定和提升工作效率(五年工作经验告诉我:最好不要乱动,否则后果自负),一般的做法是加一个if-else或新写一个方法,但这种方式随着功能的不断迭代,会变得越来越难以维护。为了尽可能的减少代码维护的压力,我认为还是有必要掌握一些主流的设计模式,在工作中如果能够用到那么几个,岂不是美滋滋。
因此本专栏将主要参考《JavaScript设计模式与开发实践》和《JavaScript高级程序设计(第4版)》并结合一些前端优秀的开源库的源码和工作中常见应用场景 介绍一些的设计模式的使用和优势,希望能通过这篇专栏能够让大家在实际的开发中用到几种设计模式,真正的掌握并能熟练使用几种设计模式。

基础知识

面向对象(OOP)

面向对象的三大要素是:继承、封装、多态,接下来我们一一介绍这三个要素。

继承

继承可以说是讨论最多的话题。很多面向对象语言都支持两种方式的继承:接口继承实现继承,而在JavaScript中,继承的方式是实现继承,通常是通过原型链实现的。
我引入《JavaScript高级程序设计(第4版)》中一段话来描述一下原型链
重温一下构造函数、原型和实例的关系,每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本思想。

封装

封装,一种将抽象性函数接口的实现细节部分包、隐藏起来的方法。封装的目的是将信息隐藏,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节、隐藏对象类型等。通常我们讨论的是封装数据封装实现

  • 封装数据
    有时,可以将一些私有的属性进行保护,防止用户直接修改或直接获取。比如Es6Class,我们可以使用privatepublicprotected关键字来提供不同的访问权限。在JavaScript中,我们可以依赖作用域来实现数据封装,例如:
var people = function() {
   var _name = 'xiaoming'
   return {
       getName() {
           return _name;
       }
   }
}()

image.png 我们可以看到,此时_name数据就变成了私有属性

  • 封装实现
    封装实现其实就是,将一些实现细节隐藏,只提供给用户一个API接口去调用,而用户也不需要关注其内部的数据变化和实现细节。这种场景在很多第三方库中可以经常看到,比如lodash提供的很多工具,用户直接调用提供的方法即可

多态

引用《JavaScript设计模式与开发实践》中对多态的解释:同一操作用于不用的对象上面,可以产生不同的解释和不同的执行结果。同时,用书中的一个例子可以更好的说明:

var makeSound = function(animal) {
    animal.sound();
}

var Duck = function() {}
Duck.prototype.sound = function() {
    console.log('嘎嘎嘎')
}

var Chicken = function() {}
Chicken.prototype.sound = function() {
    console.log('咯咯咯')
}

makeSound(new Duck()) // 嘎嘎嘎
makeSound(new Chicken()) // 咯咯咯

这个例子很好的说明了多态的含义,当我们同时让动物发出叫声(执行sound)时,Duck和Chichen会有不同的执行结果,未来如果我们需要再扩展一个Dog,那么只要再给Dog添加一个sound函数即可,并不用改动makeSound函数。

总结,多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句

设计原则

讲设计模式之前,了解设计的原则也是很有必要的。这些原则不仅可以帮助我们更容易理解设计模式,而且在实际的开发中,我们也需要依据这些原则来设计代码。
我也查阅了不少的资料,对于设计原则的分类不是很统一:

  • 一种说法是7种单一职责原则(SRP)、开闭原则(OCP)、里氏置换原则(LSP)、最少知识原则(LKP)或迪米特原则(LoD)、接口分离原则(ISP)、依赖倒置原则(DIP)、组合聚合复用原则(CARP)
  • 也有说法为5种(SOLID)单一职责原则(SRP)、开闭原则(OCP)、里氏置换原则(LSP)、接口分离原则(ISP)、依赖倒置原则(DIP)
    我们先简单介绍一下这些设计原则,后面在介绍设计模式时还会再次提起,便于大家更好的理解。

单一职责原则

单一职责就是一个对象(方法)只做一件事情。但在实际的开发中,我们应该如何去使用这一职责呢? 引用《JavaScript设计模式与开发实践》中的一句话: 要明确的是,并不是所有的职责都应该一一分离。一方面,如果随着需求的变化,两个职责总是同时变化,那就不必分离他们;另一方面,职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦合在一起,但它们还没有发生改变的征兆,那么也许没必要主动分离他们,在代码需要重构的时候再进行分离也不迟。

开闭原则

开闭原则定义:软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。我们可以用前面多态的那个例子来理解:

if-else应该是大家开发中最常用的一个逻辑了,是在实际开发中写出如下的makeSound函数是非常常见的:

var makeSound = function(animal) {
    if (animal instanceof Duck) {
        console.log('嘎嘎嘎')
    } else if (animal instanceof Chicken) {
        console.log('咯咯咯')
    }
}
var Duck = function() {}
var Chicken = function() {}

makeSound(new Duck())
makeSound(new Chicken())

那么这种写法,其实就是违反了开闭原则的一个典型例子。在前面介绍多态的时候,我们使用给DuckChicken添加sound方法后,在makeSound中只要调用其sound方法后即可。
那么,我们如何修改代码去遵守开闭原则呢?
首先,找到变化的地方把稳定不变的部分和容易变化的部分隔离开来,在以后的迭代中,我们只要修改那些容易变化的部分。同时我们可以通过hook回调函数的方式去遵守开闭原则,这里想再简单的举一个回调函数的例子:jQuery ajax中的回调jQuery.$.get(url, callback),对于这个函数来说,稳定不变的部分是发起请求的逻辑,而会变化的部分则是请求完成后的实际业务逻辑。像这类似的写法相信大家也是随处可见,在我们自己遇到类似场景时,希望大家也能够及时的想到遵守开闭原则

里氏替换原则

里氏替换原则的定义,子类拥有父类的所有方法和属性,子类可以覆盖父类的方法。 感觉和继承的概念类似,在typeScript中,class可以通过extends来继承一个父类。
举个工作的例子,比如vuex,为了区分不同的模块数据,当我们封装一个功能组件中也需要用到状态管理时,例如:一个带搜索功能和列表展示组件,在Store中会存储请求api、数据处理、搜索项数据保存、分页参数等数据,那么在这个组件使用时就需要继承组件中的Store,而对于api、数据处理等方法,我们也可以通过在子组件中重新实现进行覆盖。

最少知识原则(迪米特原则)

最少知识原则说的是一个软件实体应当尽可能少地与其他实体发生相互作用。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系。常见的做法是引入一个第三者对象,来承担这些对象之间的通信作用。其原则就是,一个对象应该对其他对象保持最少了解,保证了模块之间的解耦,同时也就提高的代码的复用性。
这个是不是和前面提到的封装很像呢?再看我们之前在封装那里举的例子,对于使用people的对象(window)来说,_name没有必要直接与它(window)产生联系,对于pepple而言,只提供需要的API,而_name对其他模块的影响很小,被影响到的机会也很小,这也是最少知识原则的体现。

接口分离原则

接口分离原则是指,在设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。也就是说,针对每一个客户类,要单独的实现其接口,而不要做一个通用的接口所有的客户类共同调用。在JavaScript中,这类场景比较少见。

依赖倒置原则

依赖倒置原则是指,模块应该依赖于抽象而不应该依赖于具体。应该如何理解呢?抽象是指一个接口或抽象类,应该更稳定;而具体是指实现接口的具体细节。其核心思想是面向接口编程。在JavaScript 中,这类场景比较少见。

组合聚合复用原则

依赖倒置原则是指,在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。也就是说,想要达到复用的目的,应尽量使用合成和聚合,而不是继承关系。这个原则感觉在JavaScript中更是少见。

总结

这篇文章大概介绍了一些面向对象的基础知识和编程几大设计原则,主要为了后面介绍设计模式做了一个铺垫,在之后的介绍中,还会在相应的位置再次提到相关内容,这样配合着设计模式的实际代码和现在的基础知识,会对这些内容有更深刻的理解。同时希望这篇文章也能够帮组到读者,不知道小伙伴们是否有了灵感,打算用到其中的几种设计原则去重构代码了~~
感谢阅读 🙏