JS面向对象最后一弹_多态篇|博客系列
# 1. 看似"多态"的一段代码
我们听的比较多的一种说法是:
多态的实际含义是:同一操作作用于不同的对象上,可以产生不同的解释和不同的执行结果。
用例子🌰来说话吧。
假设我家现在养了两只宠物,一只是小猫咪🐱,一只是小狗🐶。现在我对它们发号一个指令让它们叫。
小猫咪需要"喵喵喵~"的叫,而小狗则要"汪汪汪!"。
让它们叫就是同一操作,叫声不同就是不同的执行结果。
如果想要你转换为代码你会如何设计呢?
让我们来理一下,我们需要:
- 一个发出声音的方法
makeSound - 一个猫的类
Cat - 一个狗的类
Dog - 当调用
makeSound并且传入的是一只猫则打印出"喵喵喵~" - 当调用
makeSound并且传入的是一只狗则打印出"汪汪汪!"
那么我们很容易就得出以下这段代码了:
function makeSound (animal) {
if (animal instanceof Cat) {
console.log('喵喵喵~')
} else if (animal instanceof Dog) {
console.log('汪汪汪!')
}
}
class Cat {}
class Dog {}
makeSound(new Cat()) // '喵喵喵~'
makeSound(new Dog()) // '汪汪汪!'
当然上面👆这种做法虽然实现了我们想要的"多态性",makeSound确实是实现了传入不同类型的对象就产生不同效果的功能,但是想想如果现在我家又多养了一只小猪🐷呢?我想让它发出"啂妮妮"的叫声是不是又得去修改makeSound方法呢?
function makeSound (animal) {
if (animal instanceof Cat) {
console.log('喵喵喵~')
} else if (animal instanceof Dog) {
console.log('汪汪汪!')
} else if (animal instanceof Pig) {
console.log('啂妮妮')
}
}
class Cat {}
class Dog {}
class Pig {}
makeSound(new Cat()) // '喵喵喵~'
makeSound(new Dog()) // '汪汪汪!'
makeSound(new Pig()) // '啂妮妮'
当我们每次需要多添加一种动物的时候,都需要去改公共的方法makeSound,并不停的往里面堆砌条件分支,这显然不是我想要的。因为我们知道修改代码总是危险的,特别是修改这种带有公共性质的方法,程序出错的可能性会增大。并且随着动物种类越来越多,我们的makeSound函数的代码也会越来越多。(作为一个有职业操守的程序员我是不能忍的啊)
# 2. 对象的多态性
其实多态最根本的作用就是通过把过程化的条件语句转化为对象的多态性,从而消除这些条件分支语句。
它背后的思想通俗点来说就是把"做什么"和"谁去做以及怎么去做"分离开来。你也可以认为是把"不变的事物"和"可能改变的事物"分离开来。
这个例子🌰中既然我们已经明确了动物都会发出叫声(它就是"不变的事物"),而动物具体怎样叫是不同的(它就是"可能改变的事物"),那我们就可以把"具体怎样叫"这个动作分布到各个类上(封装到各个类上),然后在发出叫声的makeSound函数中调用"叫"这个动作就可以了。
让我们用多态的思想来改造一下上面👆的题目:
function makeSound (animal) {
if (animal.sound instanceof Function) { // 判断是否有animal.sound且该属性为函数
animal.sound()
}
}
class Cat {
sound () {
console.log('喵喵喵~')
}
}
class Dog {
sound () {
console.log('汪汪汪!')
}
}
class Pig {
sound () {
console.log('啂妮妮')
}
}
makeSound(new Cat()) // '喵喵喵~'
makeSound(new Dog()) // '汪汪汪!'
makeSound(new Pig()) // '啂妮妮'
现在我们看到的是:调用makeSound方法并传入不同的类,就会有不同的表现形式,并且后续如果我们再需要添加其它的老虎🐯、狮子🦁️、熊🐻等动物的时候也不需要再去改公共的makeSound方法了,只要这个类中有一个叫做sound的方法就可以自动执行了。并且也消除了makeSound中的各个if..else。
在这个例子🌰中我们就是把不变的部分动物都会叫(animal.sound())隔离出来,把可变的部分动物具体怎样叫的(sound(){console.log('喵喵喵~')})各自封装起来。
你还可以怎样理解它呢?
唔…猫这种动物的叫声是它与生俱来,是我在要叫它发出叫声之前就已经规定好了的,当我在需要它叫的时候它就知道自己该用"喵喵喵~"的声音叫了。
而不是像最开始那样,虽然你是只猫,但是你却不知道自己要怎样叫,在我要你发出叫声的时候并且告诉你你应该要"喵喵喵~"的叫,你才叫。
叫叫叫叫叫叫,我提莫的…是来学技术的,你给👴整绕口令呢?!嗯?!
算了,你要还不懂上面说的各种叫,我只能用另一个例子来说明这个问题了。
# 3. 拍小电影的例子
(年轻人脑袋里别总想着一些污七八糟的,这里的小电影是指成本和人员较少的小规模电影)
现在是导演,利用对象的多态性来拍摄电影,当我喊action的时候,演员开始了他的表演,摄影师开始拍摄,灯光师打好灯,各个岗位的工作人员在开拍之前就都知道自己的工作是什么要做什么了。
如果不利用对象的多态性而是利用面向过程的方式的话,当我喊出action之后,我得跑到每个人面前告诉他们他们得做什么,他们在按照我交代给他们的再做。
# 4. 强类型语言与多态
在上面👆我们一直使用的是JavaScript来进行案例讲解,这会让你觉得多态的实现似乎是一件非常容易的事情。
这个想法的产生其实源自于我们常听到的一句话:JS是一门弱类型(动态类型)语言。
(额,你没听过啊,那不好意思了,也就是我们在定义一个变量的时候可以先不指定它是什么类型的,后面赋值的时候再指定)
就像是我要宠物们叫这个案例一样。在定义makeSound方法的时候,我是传递了一个参数animal进去的,而且我并没有规定这个animal它是个什么类型的东西,比如我并没有规定它就一定是只猫,或者一定是条狗,因为在JS里可以不要我们这么做,它并没有那么严格的限制。
但是想想如果换成其它的强类型(静态)语言,来个都听过的,比如Java。
当我在定义makeSound方法的时候,它需要我指定一下animal的类型,假设我就指定为猫好了:
public class AnimalSound {
public void makeSound (Cat animal) { // Cat animal这段代码的意思就是参数 animal 的类型是一只猫
animal.sound()
}
}
现在当我们调用makeSound方法的时候并且传入了一只猫(new Cat()),是可以正常执行的。
但是如果调用makeSound方法的时候传入的是一条狗呢?程序就不允许我们这么做了,因为它规定了这只动物必须是只猫。
所以可以看出,强类型语言的类型检查在给带来安全性的同时也让我们在设计某个程序时有种不能大展身手的感觉。
那么在Java这种强类型语言中可以怎样解决这个问题呢 🤔️ ?
额,它主要是通过向上转型,先给你们来一段概念性的东西:
静态类型的面向对象语言通常被设计为可以向上转型:当给一个类变量赋值的时候,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。
就像是我们在描述宠物叫的时候,通常是说:一只小猫在叫或一只小狗在叫。但是如果忽略它具体的类型,我们可以说是:一只动物在叫。
同样的,这里我们可以设计一个类型:Animal,当猫Cat和狗Dog的类型都被隐藏在超类型Animal身后的时候,Cat和Dog就能被交换着使用了。
这是让对象表现出多态性的必经之路,而多态性的表现正是实现众多设计模式的目标。
这样平白的说好像有些枯燥,让我们看看下面TypeScript的例子。
# 5. TypeScript中使用继承得到多态效果
如果你觉得Java你看不懂也不想看的话,那我就以TypeScript来给你举例。众所周知它是能让我们写"静态类型的JS"。我用它来实现静态类型的向上转型。
(额,不过TypeScript在使用上和一些真正的静态类型语言还是有区别的,这里我只是为了方便你理解所以用它来编写案例)
现在我用上面👆提到的东西,给makeSound方法指定一个类型Animal,它规定了传入的对象必须要会叫才行:
(不会TypeScript没关系,但以此为理由不去看下面的内容就不行了,我会尽量说的详细一些 😊)
interface Animal {
sound(): void
}
function makeSound (animal: Animal) {
animal.sound()
}
makeSound方法中的参数的意思简单理解就是:
- 需要传入一个
animal的对象,这个对象必须是Animal类型的 - 而
Animal类型(用interface定义的称之为接口)的限制就是必须要有sound方法 - 所以我们可以得出:传入的
animal对象必须要有sound方法
(sound(): void表示的是定义一个名为sound的方法且这个方法的返回值为空)
所以现在让我们来设计一下Cat和Dog:

就像上面这段代码,我把Dog的sound方法隐掉了,那么在调用makeSound的时候类型检查就已经错了,它要求我们传入的对象必须要有sound属性。
(扩展一下,当然你也可以在tsconfig.json中修改一下配置:"noImplicitAny": false,设置了这个属性之后,你的function makeSound(animal) {} 的参数animal可以不用必须指定一个类型,不过这样的话就失去了我讲解这个案例的意义了)
对于上面这种情况,我们就可以使用接口继承来设计这么一个多态的功能:
interface Animal {
sound(): void
}
function makeSound (animal: Animal) {
animal.sound()
}
class Cat implements Animal { // 使用 implements 继承接口 Animal
sound(): void {
console.log('喵喵喵~')
}
}
class Dog implements Animal {
sound(): void {
console.log('汪汪汪!')
}
}
makeSound(new Cat())
makeSound(new Dog())
我们看到了一个生疏的API:implements,英文意思:实现、执行、落实。
它的作用其实和extends差不多,只不过extends后面接着的是一个类,而implements规定继承的是一个接口(也就是用interface声明的东西)
使用了implements接口继承的Cat和Dog就表示这两个类中必须要有sound方法才行。
比如我又把Dog中的sound去掉:

现在在我定义Dog这个类的时候就已经给出错误提示了。
# 6. JS与身俱来的多态性
从上面👆的几个案例我们可以看出,多态的思想实际上是要把"做什么"与"谁去做"分离开来,那么要实现这一点,最主要的一点就是要先消除类型之间的耦合关系。
