你盼世界,我盼望你无bug
。Hello 大家好!我是霖呆呆!
首先抱歉让大家久等了,大家一直期待的"多态"
篇到现在才来 😊,其实我最近挺忙的,给张手机截图让大家感受一下。
算了,说了你们也不行。
因为平常开发中感觉多态听到的不多,所以就会让我们产生一个误区:多态并不重要。但其实它在各种设计模式中却有着重要的作用,绝大多数设计模式都离不开多态性的思想。
不过本篇文章不会过于深入的讨论设计模式,主要是让你能了解多态的概念,以及可以用怎样的方式来实现一些多态,看完之后希望对你们能有所帮助。
该系列主要为了让我们彻底理解JavaScript
面向对象的三大特性:封装
、继承
、多态
。
系列总目录:
- 🔥【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)
- 💦【何不三连】做完这48道题彻底弄懂JS继承(1.7w字含辛整理-返璞归真)
- 🍃【何不三连】JS面向对象最后一弹-多态篇(羽化升仙) 「本文」
这一章节是讲解JS
面向对象的最后一大特性-多态。
通过阅读本章节你可以学习到:
- 看似"多态"的一段代码
- 对象的多态性
- 拍小电影的例子
- 强类型语言与多态
- TypeScript中使用继承得到多态效果
- JS与身俱来的多态性
- 多态的实际应用
我们听的比较多的一种说法是:
多态的实际含义是:同一操作作用于不同的对象上,可以产生不同的解释和不同的执行结果。
用例子🌰来说话吧。
假设我家现在养了两只宠物,一只是小猫咪🐱,一只是小狗🐶。现在我对它们发号一个指令让它们叫。
小猫咪需要"喵喵喵~"
的叫,而小狗则要"汪汪汪!"
。
让它们叫就是同一操作,叫声不同就是不同的执行结果。
如果想要你转换为代码你会如何设计呢?
让我们来理一下,我们需要:
- 一个发出声音的方法
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
函数的代码也会越来越多。(作为一个有职业操守的程序员我是不能忍的啊)
其实多态最根本的作用就是通过把过程化的条件语句转化为对象的多态性,从而消除这些条件分支语句。
它背后的思想通俗点来说就是把"做什么"
和"谁去做以及怎么去做"
分离开来。你也可以认为是把"不变的事物"
和"可能改变的事物"
分离开来。
这个例子🌰中既然我们已经明确了动物都会发出叫声
(它就是"不变的事物"
),而动物具体怎样叫
是不同的(它就是"可能改变的事物"
),那我们就可以把"具体怎样叫"
这个动作分布到各个类上(封装到各个类上),然后在发出叫声的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('喵喵喵~')}
)各自封装起来。
你还可以怎样理解它呢?
唔...猫这种动物的叫声是它与生俱来,是我在要叫它发出叫声之前就已经规定好了的,当我在需要它叫的时候它就知道自己该用"喵喵喵~"
的声音叫了。
而不是像最开始那样,虽然你是只猫,但是你却不知道自己要怎样叫,在我要你发出叫声的时候并且告诉你你应该要"喵喵喵~"
的叫,你才叫。
叫叫叫叫叫叫,我提莫的...是来学技术的,你给👴整绕口令呢?!嗯?!
算了,你要还不懂上面说的各种叫,我只能用另一个例子来说明这个问题了。
(年轻人脑袋里别总想着一些污七八糟的,这里的小电影是指成本和人员较少的小规模电影)
现在霖呆呆是导演,利用对象的多态性来拍摄电影,当我喊action
的时候,演员开始了他的表演,摄影师开始拍摄,灯光师打好灯,各个岗位的工作人员在开拍之前就都知道自己的工作是什么要做什么了。
如果不利用对象的多态性而是利用面向过程的方式的话,当我喊出action
之后,我得跑到每个人面前告诉他们他们得做什么,他们在按照我交代给他们的再做。
在联想一下上面那个例子,是不是这样的呢?
在上面👆我们一直使用的是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
的例子。
如果你觉得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
这个类的时候就已经给出错误提示了。
从上面👆的几个案例我们可以看出,多态的思想实际上是要把"做什么"
与"谁去做"
分离开来,那么要实现这一点,最主要的一点就是要先消除类型之间的耦合关系。
这什么耦合关系
其实就是我们前面Java
案例中的那种,一旦我们指定了makeSound
方法的参数animal
是一个Cat
之后,那animal
这个参数就不能传入一只Dog
的对象。即:一旦我们指定了方法的参数是某个类型,它就不能再被替换为另一个类型。
而通过TypeScript
那个案例我们又可以知道对于Java
这种静态类型语言可以通过向上转型来实现多态。
但是对于JS
呢?它的变量类型在运行期是可变的,我的animal
参数即可以是Cat
也可以是Dog
,程序并没有要求我指定它的类型,也就是它并不存在这种类型之间的耦合关系,所以我说它的这种多态性是与生俱来的。
这也可以看出,一个东西能否叫,只取决于它是否有sound
方法,而不取决于它是否是某种类型的对象。
例如下面这个案例🌰,我设计了一个Phone
类,它并不是一个动物类型的,但是它有sound
方法,所以它也能叫:
在JS
中并不需要诸如向上转型之类的技术来取得多态的效果。
如果你没有接触过一些设计模式的话,可能就没有感受到它的重要性。实际上绝大多数设计模式的实现都离不开多态性的思想,例如组合模式、策略模式等等。(关于设计模式这部分的内容霖呆呆也会写个它们的专题系列 😊,不过可能没那么快)
在实际工作上来说,霖呆呆没怎么用过,所以不敢妄下结论说它用的不多。这边在网上是搜寻了一下它的实际应用,十有八九的案例都是和《JavaScript设计模式与开发实践》一书中的百度地图谷歌地图的案例一样。
怎么回事现在的年轻人,就不能有点新创意新想法?
我就不用百度和谷歌地图,我就用高德和搜狗地图!硬气~
案例是这样的,其实和我开始列举的命令宠物们叫
差不多,你可以看下题目然后自个想想解法。
假设我们现在要编写一个地图应用,有两家可选的地图API提供商供我们接入自己的应用。但是我们不确定实际用会用哪一家,而两家地图的API都提供了一个show
方法,负责在页面上展示整个地图。请你利用多态的思想设计一个showMap
方法,传入不同的地图供应商进去渲染出不同的地图。
- 高德:GaodeMap
- 搜狗:SogouMap
代码如下:
class GaodeMap {
show () {
console.log('高德地图持续为您导航')
}
}
class SogouMap {
show () {
console.log('欢迎使用搜狗地图')
}
}
function showMap (map) {
if (map.show instanceof Function) {
map.show()
}
}
showMap(new GaodeMap())
showMap(new SogouMap())
知识无价,支持原创。
参考文章:
- 《浅谈JavaScript的面向对象和它的封装、继承、多态》
- 《Js三大特性--封装、继承以及多态》
- 《JavaScript设计模式与开发实践》
你盼世界,我盼望你无bug
。这篇文章就介绍到这里。
喜欢霖呆呆的小伙还希望可以关注霖呆呆的公众号 LinDaiDai
或者扫一扫下面的二维码👇👇👇.
我会不定时的更新一些前端方面的知识内容以及自己的原创文章🎉
你的鼓励就是我持续创作的主要动力 😊.
相关推荐:
《【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理)》
《【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)》
《【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)》