深入理解面向对象 -- 基于 JavaScript 实现
我们在学习编程时,避免不了会接触一个概念,叫:面向对象编程(Object-oriented programming,缩写:oop)
(不是搞对象那个对象哈),其实我们的编程方式,不止有面向对象,还有 面向过程编程
、面向流编程
、面向函数编程
、面向接口编程
等。作为一名一直混迹在前端的小菜鸟,今天就来跟大家深入的探讨一下 JavaScript面向对象
。作为程序员,我们多多少少都会接触自己擅长语言之外的编程语言,比如我作为一名前端,同时我还会 Java
,从这两个语言本身出发的话,我们会发现这两种语言的 面向对象
存在着一丝丝的不同,到底哪里不同呢?我们今天就拿这两种语言对比着来,拿具体的实例看一下,到底什么叫 面向对象编程
。
现在很多文章都会讲 面向对象三大特性
、面向对象七原则
、设计模式
等概念,今天这篇文章不准备讲这些概念,从实例出发,理解 面向对象
是什么,如何做 面向对象
程序设计。
我们在深入探讨 面向对象
之前,我们先来复习一下 面向过程编程
,这里可能有人会问了,不是讲 面向对象
吗?为什么还要讲 面向过程
呢?主要是因为,面向过程编程
是软件思想中的鼻祖。面向过程编程
还是很好理解的,因为它是一种以 过程
作为中心的编程思想,其中 过程
的含义就是 完成一件事情的步骤
。
面向过程
其实是一种 机械的思想
,它就像流水线一样,一个阶段衔接一个阶段,每个阶段都有自己的输入、处理、输出,而在流水线上流动的就是我们的原料或者中间产品,每个阶段都有一个机械进行处理,最后的输出就是我们的产品。
在运用 面向过程
的方法时,你也需要设计这样一条程序:将程序划分为不同的阶段,设计好各个阶段如何衔接,然后定义好每个阶段需要处理的数据。
在实际开发中,我们会把需求拆成一个一个的命令,然后串起来交给计算机去执行。举个例子,有个需求是:在淘宝给女朋友买口红
,那么程序员接到这个命令,会列出如下几个步骤:
- 打开淘宝
- 买口红
- 送女朋友
上面的每一个步骤,程序员都会用一个 函数
或 方法
来实现,而 函数
或 方法
是一些代码的集合体,每个 函数
或 方法
可以实现一个功能,那么根据上述需求,我们可能会定义如下的函数:
- openTaoBao();
- buyLipstick();
- sendGrilFriend();
那么程序就会顺序调用了。需求完成,顺利交工。但是,你觉得这样就算结束了么?No。产品经理说:"这才刚刚开始哦~~~"。
在开始介绍 面向对象
之前,我们先来简单概述一下,什么是 对象
?对象
是一个自成一体的实体,它仅包含属性和行为,不含任何其他内容。与面向过程
的方法相比,面向对象
不在局限于计算机的机器本质,而更加侧重于对现实世界的 模拟
。在 面向过程
的方法中,有一套设计严格的操作顺序,有一个类似 中央控制器
的角色来进行统一调度;而 面向对象
的方法中,并没有明确的 中央控制器
的角色,也不需要指定严格的操作循序,而是设计了很多 对象
,并且指定了这些 对象
需要完成的任务,以及这些 对象
如何对外界的刺激做出反应。
如果说 面向过程
像一条流水线,那么 面向对象
就像是一个篮球队。没有哪个人能够在一场比赛开始的时候,就精确指定每个队员的每一次跑动、每一次传球、每一次投篮...而是要指定队员的角色(前锋、中锋、后卫等等),然后由队员们自己根据情况做出反应。所以说,世界上可以有两个一模一样的生产线,但绝对不会存在两场一模一样的比赛。
简单介绍了一下 对象
,现在让我们回到上面的例子。接下来,产品经理又提了需求:
- 在京东给女朋友买防晒霜
- 在唯品会给麻麻买貂
- 在苏宁易购给爸爸买刮胡刀
- ...
如果我们还是用 面向过程
的方法,每次需求的变更,程序员就要把整个系统通读一遍,找出可用的函数(如果没有就再定义一个),最后依次调用它们。最后系统越来越杂乱无章难以管理,程序员不堪重负,纷纷操起刀走上了不归路[笑哭]...
面向对象
从另一个角度来解决这个问题,它抛弃了函数,把 对象
作为程序的基本单元。那么 对象
到底是个什么东西呢?对象
就是对 事务
的一种 抽象
描述。其实现实中的 事务
,都可以用 数据
和 能力
来描述。比如我要描述一个人,数据
就是他的年龄、性别、身高、体重等,能力
就是他能做什么工作,承担什么样的责任。描述一台电视,数据
就是它的屏幕尺寸、亮度,能力
就是播放青春偶像剧。
面向对象
的世界里,到处都是 对象
。对象
不光有 数据
和 能力
,还可以接受命令。例如,你可以让 猫
这个对象 吃猫粮
,就可以把 吃猫粮
的命令发给 猫
让其执行(虽然傲娇的猫咪并不能听你的话吧[笑哭],这里只是举个例子),然后我们就实现了 猫吃猫粮
的需求。
现在 对象
有了,那接下来该如何进行 面向对象
的编程呢?其实很简单,我们依次向不同的 对象
发送命令就可以了。回到上面的例子,我们用 面向对象
来实现;先定义一个 app
对象,它的 数据
就是商城名称、商品类型等,能力
就是打开、关闭;还有一个 人
对象,它的 数据
是姓名、性别、称谓等,能力
就是买口红、送口红。然后我们依次下达命令:
- 向app下达
打开
的命令; - 向人下达
买口红
、送女朋友
的命令; - 向app下达
关闭
的命令。
其实,我们创建的对象,应该是刚刚好能做完它能做的事情,不多做,也不少做。多做了容易耦合,各种功能杂糅在一个对象里。比如我有一个对象叫 汽车
,可以 载人
,现在的需求是要实现 载人飞行
,就不能重用这个 对象
,必须新定义一个对象 飞机
来做。如果你给 汽车
插上了翅膀,赋予了它 飞行
的能力,那么新来的同学面对你的代码会莫名其妙,无从下手。
接下来,我们来看一下,上面的例子用代码是如何实现的:
- 首先要创建一个
App
的对象,里面包含商城名称的数据,打开和关闭的能力:
1 function App(shopName) {
2 this.shopName = shopName;
3 }
4
5 App.prototype.open = function () {
6 return `打开${this.shopName}`;
7 };
8
9 App.prototype.close = function () {
10 return `关闭${this.shopName}`;
11 };
- 接着我们创建一个
人
的对象,里面包含称谓的数据,买和送的能力:
1 function Person(title) {
2 this.title = title;
3 }
4
5 Person.prototype.buy = function (product) {
6 return `买${product}`;
7 };
8
9 Person.prototype.send = function () {
10 return `送给${this.title}`;
11 };
- 最后我们实例化对象,然后聚合我们需要的功能:
1 const app = new App('淘宝');
2 console.log(app.open());
3 const person = new Person('女朋友');
4 console.log(person.buy('口红'));
5 console.log(app.close());
6 console.log(person.send());
- 我们来看一下最后的执行结果:
基于上面的例子,我们可以看到,JavaScript
的 面向对象
是基于 原型
的,也就是 prototype
,而 Java
呢?Java
是基于 类
的,也就是所谓的 class
。其实不管语言对于 面向对象
是基于什么的,从概念上讲,大家都是一样的,只是我们的实现方式不同。
在这里,我就不举 Java
基于 面向对象
是如何实现上述实例的了,因为这篇文章讲的就是 JavaScript
[斜眼笑],想看 Java
的可以根据上述的文字描述自己实现一下哈,博主在这里就皮一下[笑哭]。
抽象
抽象
的中文概念非常形象,简单来说就是 抽取出来比较像的部分
。那么,在 面向对象
的领域里,抽取什么东西是比较像的部分?我们画个图来看一下 抽象
是个什么东东:
这里的抽象分为两个层次:
第一个层次:对象是抽象成集合(类)
例如:西瓜
和 苹果
抽象成 水果
,这一层的 抽象
主要是将 属性类似
的对象抽象出来。
注意:这里的
属性类似
是指属性类别
一致,而属性的取值是不一样的。例如,将"西瓜"和"苹果"都抽象成"水果",那么其属性有颜色、重量、味道等等,但"西瓜"和"苹果"的这些属性取值肯定是不同的。
第二个层次(或更高层次):将对象抽象为超集合(超类,或者说父类,就是更高一级的集合或者类)
例如:水果
和 蔬菜
抽象成 食物
,这一层的抽象主要是将 行为类似
的抽象成父集合(父类)。
注意:这里是
行为类似
,而不是第一层抽象的那样属性类似
,因为在面向对象
领域,行为一致的话就认为是同一类的,当然也不能是完全不同,完全不同的话就没有相似点,也就无法抽象成类了,所以这一层抽象的重点是相似
。
在实际应用中,抽象的层次是不限的,根据业务需要,或者不同的观察角度,可以抽象出很多层。
抽象的作用
抽象
并不是面向对象领域特有的概念和方法,在我们的日常生活和学习中,抽象
最主要的作用是 划分类别
,而 划分类别
的主要目的其实还是关注隔离点,降低复杂度。所以,抽象是面向对象领域里面发现集合(类)的主要方法。
在JavaScript中,
抽象
是允许模拟工作问题中通用部分的一种机制。这可以通过继承(具体化)或组合来实现。JavaScript通过继承
实现具体化
,通过让类的实例是其他对象的属性值来实现组合。
JavaScript Function类
继承自Object类(这是典型的具体化)
。Function.prototype
的属性是一个Object实例(这是典型的组合)
。
多态(polymorphism)
引用 MDN web docs
中的一段话来描述一下 JavaScript多态
:
就像所有定义在原型属性内部的
方法
和属性
一样,不同的类可以定义具有相同名称的方法;方法是作用于所在的类中。并且这仅在这两个类不是父子关系时成立(继承链中,一个类不是继承自其他类)。
[笑哭]大家看完这段话之后,是不是觉得很懵,这是在说什么啊,什么类,什么继承。不着急哈,接下来我会详细解释一下在 JavaScript
中,多态究竟是怎么样的存在哈...
polymorphism
,翻译成中文:多态性
,我们从字面意思上就可以看出,多态
就是 多种形态
的意思。但仔细探究一下:多种形态
其实还是没法很好的理解,不同的人也还是有不同的理解。
动画片看得多的同学可能会以为:
多种形态
,就是很多种变身,就像孙悟空72变一样,一会儿可以变成房子,一会儿可以变成牛魔王;
擅长打扮的美女可能会以为:多种形态
,其实就是换不同的衣服嘛,一会儿文艺小清新打扮,一会儿高贵典雅的贵妇装束;
学院派技术宅男可能会以为:多种形态
,其实就是多种状态啦,比如说TCP协议栈有XX种状态...
可能还有很多其它各种各样的理解,但在 面向对象
领域,这些理解都不正确,多态不是变身、换装、状态变化,而是多胎...
哇!!博主你打错字了,怎么可能是 多胎
呢?这是什么意思啊?
其实,多胎
在这里也是一个形象的说法,在 面向对象
领域,多态
的真正含义是:使用指向父类的指针或者引用,能够调用子类的对象。
我要是在这里引用 Java
代码,会不会引起公愤[笑哭],还是乖乖的用 JavaScript
来写个 多态
的例子:
- 首先建一个
Person
对象:
1 // 定义Person构造器(类)
2 function Person(personName) {
3 this.personName = personName;
4 }
5
6 // 在Person.prototype中加入study方法
7 Person.prototype.study = function () {
8 return `${this.personName}学习语文`;
9 };
- 然后创建一个
Boy
对象,并且继承自Person
对象,修改原先Person
对象中的方法:
1 // 定义Boy构造器(类)
2 function Boy(personName) {
3 // 调用父类构造器,确保"this"在调用过程中设置正确
4 Person.call(this, personName);
5 }
6
7 //建立一个由Person.prototype继承而来的Boy.prototype对象
8 Boy.prototype = Object.create(Person.prototype);
9
10 // 设置"constructor"属性指向Boy
11 Boy.prototype.constructor = Boy;
12
13 // 更换"study"方法
14 Boy.prototype.study = function () {
15 return `${this.personName}学习数学`;
16 };
- 再然后,创建一个
Girl
对象,也让它继承自Person
对象,继续修改原先Person
对象中的方法:
1 // 定义Girl构造器(类)
2 function Girl(personName) {
3 Person.call(this, personName);
4 }
5
6 //建立一个由Person.prototype继承而来的Girl.prototype对象
7 Girl.prototype = Object.create(Person.prototype);
8
9 // 设置"constructor"属性指向Girl
10 Girl.prototype.constructor = Girl;
11
12 // 更换"study"方法
13 Girl.prototype.study = function () {
14 return `${this.personName}学习英语`;
15 };
- 创建一个执行函数:
1 // 这个参数就是"多态"的具体表现形式
2 const test = function (person) {
3 // 在调用person.study()的时候,函数并不知道person究竟是Boy,还是Girl,只知道是个对象
4 console.log(person.study());
5 };
6
7 // 执行test方法
8 test(new Boy('Tom'));
9 test(new Girl('Jenny'));
- 最后我们看一下执行结果:
嗯,没错,是我们想要的结果[嘿嘿]。那接下来,我们在来看一下,如果不用 多态
,上述的例子要怎么写。
- 好,先创建一个执行对象那个:
1 // 定义一个执行的构造器(类)
2 function Test() {}
3
4 // 在Test.prototype原型中加入boyStudy方法
5 Test.prototype.boyStudy = function (boy) {
6 console.log(boy.study());
7 };
8
9 // 在Test.prototype原型中加入girlStudy方法
10 Test.prototype.girlStudy = function (girl) {
11 console.log(girl.study());
12 };
- 在创建一个
Boy
对象:
1 function Boy(boyName) {
2 this.boyName = boyName;
3 }
4
5 Boy.prototype.study = function () {
6 return `${this.boyName}学习数学`;
7 };
- 在创建一个
Girl
对象:
1 function Girl(girlName) {
2 this.girlName = girlName;
3 }
4
5 Girl.prototype.study = function () {
6 return `${this.girlName}学习英语`;
7 };
- 最后,我们调用执行对象,输出这两个对象的数据:
1 const test = new Test();
2
3 test.boyStudy(new Boy('Tom'));
4
5 test.girlStudy(new Girl('Jenny'));
当然最后的执行结果肯定是一样的,那让我们来看一下,这两种写法到底有什么区别:
- 首先,最开始的那个例子,我们用了个
test
函数,注释也写了,不需要关心对象具体是哪个,只要对象包含需要调用的方法就OK; - 而第二个例子呢?我们仔细看一下,第二个例子现在看起来很清晰,但是不利于扩展,为什么?关键点在执行函数中,如果我要是在加了
女人
、男人
这两个对象的话,那是不是还得在Test
对象里面在添加两个对应的执行方法?答案是肯定的,不然没地方执行呀。 - 所以说,这就是
多态
的特点。
有兴趣的同学还可以用 es6
语法的 class
、extends
来写一下上述的例子,我这里就不在赘述了。
我们说了这么多,主要是想讲述一下在 JavaScript
中,是怎么体现这些思想的,我最想说的一句话就是:JavaScript
是基于 原型
的语言,也希望大家能一直记住这句话。
而 Java 呢,是基于 类(class) 的语言,这两种语言从语法上就有本职的区别,但是概念性的东西,是不会变的。我们应该抛开语言层面,更进一步的去学习面向对象的概念,然后在从语言上下手,学习如何实现这一概念。
啊哈,对了,突然想到,虽然现在对于前端来说,JavaScript
挺重要的,但是我们大多数同学在开发时,会用到三大框架的其中之一(react、vuee、angular),也有可能都用过,我们大多数人把关注点都放在了 JavaScript
,而忽略了我们的 html + css
也是有 面向对象
概念的。今天就拿 vue
中的组件概念,来简单说说 模板
是如何实现 面向对象
的。
不知道用 vue
做过开发的同学们,记不记得组件有个概念叫 动态组件
[斜眼笑]。对,就是那个 <component></component>
,需要通过 is
属性来加载不同的组件。这里就不在具体举例子讲这个东西怎么用了,但是它就是 多态
的一种的实现形式,component
不会关心你的组件都有什么(作用相当于上述js例子的test函数),知道你传过来的数据,我能匹配上,找到你想要的那个结果就好了。官方举的例子是关于 tab
进行动态切换时,不管加几个标签,对本身功能并不会有影响,只需要多建几个模板就好了。
好了,就讲这么多吧,对 Java
感兴趣的同学,可以根据上述的例子,用 Java
来写一下,体验一下 面向对象
、多态
的快感,啊哈哈~~~