深入理解面向对象 -- 基于 JavaScript 实现

深入理解面向对象 -- 基于 JavaScript 实现

我们在学习编程时,避免不了会接触一个概念,叫:面向对象编程(Object-oriented programming,缩写:oop) (不是搞对象那个对象哈),其实我们的编程方式,不止有面向对象,还有 面向过程编程面向流编程面向函数编程面向接口编程 等。作为一名一直混迹在前端的小菜鸟,今天就来跟大家深入的探讨一下 JavaScript面向对象。作为程序员,我们多多少少都会接触自己擅长语言之外的编程语言,比如我作为一名前端,同时我还会 Java,从这两个语言本身出发的话,我们会发现这两种语言的 面向对象 存在着一丝丝的不同,到底哪里不同呢?我们今天就拿这两种语言对比着来,拿具体的实例看一下,到底什么叫 面向对象编程

现在很多文章都会讲 面向对象三大特性面向对象七原则设计模式 等概念,今天这篇文章不准备讲这些概念,从实例出发,理解 面向对象 是什么,如何做 面向对象 程序设计。

我们在深入探讨 面向对象 之前,我们先来复习一下 面向过程编程,这里可能有人会问了,不是讲 面向对象 吗?为什么还要讲 面向过程 呢?主要是因为,面向过程编程 是软件思想中的鼻祖。面向过程编程 还是很好理解的,因为它是一种以 过程 作为中心的编程思想,其中 过程 的含义就是 完成一件事情的步骤

面向过程 其实是一种 机械的思想,它就像流水线一样,一个阶段衔接一个阶段,每个阶段都有自己的输入、处理、输出,而在流水线上流动的就是我们的原料或者中间产品,每个阶段都有一个机械进行处理,最后的输出就是我们的产品。

在运用 面向过程 的方法时,你也需要设计这样一条程序:将程序划分为不同的阶段,设计好各个阶段如何衔接,然后定义好每个阶段需要处理的数据。

在实际开发中,我们会把需求拆成一个一个的命令,然后串起来交给计算机去执行。举个例子,有个需求是:在淘宝给女朋友买口红,那么程序员接到这个命令,会列出如下几个步骤:

  • 打开淘宝
  • 买口红
  • 送女朋友

上面的每一个步骤,程序员都会用一个 函数方法 来实现,而 函数方法 是一些代码的集合体,每个 函数方法 可以实现一个功能,那么根据上述需求,我们可能会定义如下的函数:

  • openTaoBao();
  • buyLipstick();
  • sendGrilFriend();

那么程序就会顺序调用了。需求完成,顺利交工。但是,你觉得这样就算结束了么?No。产品经理说:"这才刚刚开始哦~~~"。

在开始介绍 面向对象 之前,我们先来简单概述一下,什么是 对象对象 是一个自成一体的实体,它仅包含属性和行为,不含任何其他内容。与面向过程的方法相比,面向对象 不在局限于计算机的机器本质,而更加侧重于对现实世界的 模拟。在 面向过程 的方法中,有一套设计严格的操作顺序,有一个类似 *控制器 的角色来进行统一调度;而 面向对象 的方法中,并没有明确的 *控制器 的角色,也不需要指定严格的操作循序,而是设计了很多 对象,并且指定了这些 对象 需要完成的任务,以及这些 对象 如何对外界的刺激做出反应。

如果说 面向过程 像一条流水线,那么 面向对象 就像是一个篮球队。没有哪个人能够在一场比赛开始的时候,就精确指定每个队员的每一次跑动、每一次传球、每一次投篮...而是要指定队员的角色(前锋、中锋、后卫等等),然后由队员们自己根据情况做出反应。所以说,世界上可以有两个一模一样的生产线,但绝对不会存在两场一模一样的比赛。

简单介绍了一下 对象,现在让我们回到上面的例子。接下来,产品经理又提了需求:

  • 在京东给女朋友买防晒霜
  • 在唯品会给麻麻买貂
  • 在苏宁易购给爸爸买刮胡刀
  • ...

如果我们还是用 面向过程 的方法,每次需求的变更,程序员就要把整个系统通读一遍,找出可用的函数(如果没有就再定义一个),最后依次调用它们。最后系统越来越杂乱无章难以管理,程序员不堪重负,纷纷操起刀走上了不归路[笑哭]...

面向对象 从另一个角度来解决这个问题,它抛弃了函数,把 对象 作为程序的基本单元。那么 对象 到底是个什么东西呢?对象 就是对 事务的一种 抽象 描述。其实现实中的 事务,都可以用 数据能力 来描述。比如我要描述一个人,数据 就是他的年龄、性别、身高、体重等,能力 就是他能做什么工作,承担什么样的责任。描述一台电视,数据 就是它的屏幕尺寸、亮度,能力 就是播放青春偶像剧。

面向对象 的世界里,到处都是 对象对象 不光有 数据能力 ,还可以接受命令。例如,你可以让 这个对象 吃猫粮,就可以把 吃猫粮 的命令发给 让其执行(虽然傲娇的猫咪并不能听你的话吧[笑哭],这里只是举个例子),然后我们就实现了 猫吃猫粮 的需求。

现在 对象 有了,那接下来该如何进行 面向对象 的编程呢?其实很简单,我们依次向不同的 对象 发送命令就可以了。回到上面的例子,我们用 面向对象 来实现;先定义一个 app 对象,它的 数据 就是商城名称、商品类型等,能力 就是打开、关闭;还有一个 对象,它的 数据 是姓名、性别、称谓等,能力 就是买口红、送口红。然后我们依次下达命令:

  • 向app下达 打开 的命令;
  • 向人下达 买口红送女朋友 的命令;
  • 向app下达 关闭 的命令。

其实,我们创建的对象,应该是刚刚好能做完它能做的事情,不多做,也不少做。多做了容易耦合,各种功能杂糅在一个对象里。比如我有一个对象叫 汽车,可以 载人,现在的需求是要实现 载人飞行,就不能重用这个 对象,必须新定义一个对象 飞机 来做。如果你给 汽车 插上了翅膀,赋予了它 飞行的能力,那么新来的同学面对你的代码会莫名其妙,无从下手。

接下来,我们来看一下,上面的例子用代码是如何实现的:

  1. 首先要创建一个 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. 接着我们创建一个 的对象,里面包含称谓的数据,买和送的能力:
 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. 最后我们实例化对象,然后聚合我们需要的功能:
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());
  1. 我们来看一下最后的执行结果:

基于上面的例子,我们可以看到,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 来写个 多态 的例子:

  1. 首先建一个 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 };
  1. 然后创建一个 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 };
  1. 再然后,创建一个 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. 创建一个执行函数:
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. 最后我们看一下执行结果:

嗯,没错,是我们想要的结果[嘿嘿]。那接下来,我们在来看一下,如果不用 多态,上述的例子要怎么写。

  1. 好,先创建一个执行对象那个:
 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 };
  1. 在创建一个 Boy 对象:
1 function Boy(boyName) {
2     this.boyName = boyName;
3 }
4 
5 Boy.prototype.study = function () {
6     return `${this.boyName}学习数学`;
7 };
  1. 在创建一个 Girl 对象:
1 function Girl(girlName) {
2     this.girlName = girlName;
3 }
4 
5 Girl.prototype.study = function () {
6     return `${this.girlName}学习英语`;
7 };
  1. 最后,我们调用执行对象,输出这两个对象的数据:
1 const test = new Test();
2 
3 test.boyStudy(new Boy('Tom'));
4 
5 test.girlStudy(new Girl('Jenny'));

当然最后的执行结果肯定是一样的,那让我们来看一下,这两种写法到底有什么区别:

  • 首先,最开始的那个例子,我们用了个 test 函数,注释也写了,不需要关心对象具体是哪个,只要对象包含需要调用的方法就OK;
  • 而第二个例子呢?我们仔细看一下,第二个例子现在看起来很清晰,但是不利于扩展,为什么?关键点在执行函数中,如果我要是在加了 女人男人 这两个对象的话,那是不是还得在 Test 对象里面在添加两个对应的执行方法?答案是肯定的,不然没地方执行呀。
  • 所以说,这就是 多态 的特点。

有兴趣的同学还可以用 es6 语法的 classextends 来写一下上述的例子,我这里就不在赘述了。

我们说了这么多,主要是想讲述一下在 JavaScript 中,是怎么体现这些思想的,我最想说的一句话就是:JavaScript 是基于 原型 的语言,也希望大家能一直记住这句话。

而 Java 呢,是基于 类(class) 的语言,这两种语言从语法上就有本职的区别,但是概念性的东西,是不会变的。我们应该抛开语言层面,更进一步的去学习面向对象的概念,然后在从语言上下手,学习如何实现这一概念。

啊哈,对了,突然想到,虽然现在对于前端来说,JavaScript 挺重要的,但是我们大多数同学在开发时,会用到三大框架的其中之一(react、vuee、angular),也有可能都用过,我们大多数人把关注点都放在了 JavaScript,而忽略了我们的 html + css 也是有 面向对象 概念的。今天就拿 vue 中的组件概念,来简单说说 模板 是如何实现 面向对象 的。

不知道用 vue 做过开发的同学们,记不记得组件有个概念叫 动态组件[斜眼笑]。对,就是那个 <component></component>,需要通过 is 属性来加载不同的组件。这里就不在具体举例子讲这个东西怎么用了,但是它就是 多态 的一种的实现形式,component 不会关心你的组件都有什么(作用相当于上述js例子的test函数),知道你传过来的数据,我能匹配上,找到你想要的那个结果就好了。官方举的例子是关于 tab 进行动态切换时,不管加几个标签,对本身功能并不会有影响,只需要多建几个模板就好了。

好了,就讲这么多吧,对 Java 感兴趣的同学,可以根据上述的例子,用 Java 来写一下,体验一下 面向对象多态 的快感,啊哈哈~~~