浅谈原型继承和类继承

在学习了Java后,我自认为对面向对象编程有所了解,但当我开始探索JavaScript,我领悟到全新的『计算机艺术大师们的设计思想』


继承,这是面向对象编程(OOP)的三大核心思想之一,而将OOP思想严格而规范地体现在语法中的编程语言,在我为数不多有所接触的几门语言中,当属Java拔得头筹。这种class entends class的模式非常符合人们对生产生活需求的建模,尽管对于当时粗浅地学了一下C语言的我来说很难一下子去理解这种思想的优势,等到实际写过了不少代码后,接触到了一些需求,我才算是悟到了这种思想的精髓所在,以至于如今我看到一个需求时会下意识用Java的设计理念去思考,而从面向过程的角度来建模则觉得有些困难了。所以我在学习JavaScript的面向对象时,下意识地也将其与Java类比,但却不曾意识到JavaScript的所谓原型继承对我来说是一个全新的概念,与Java所代表的类继承共同打下了编程语言设计关于代码复用的一座江山(另一座是组合),是两派计算机科学家不同设计理念的杰作。

类继承

与地球生物数亿年来的演化相似,『类』这个概念也是编程语言几十年的发展过程中逐渐形成的。那些教程总是说“类是对象的模板,对象是类的实例”,到底什么是模板和实例呢?

最初的编程语言,大概只提供一些基本的变量声明和赋值、流程控制、函数等等,很快人们发现很多数据其实存在内在的关联,比如一个长方形有它的宽高,所以后来程序语言有了结构体这种东西,可以将宽高定义到一个长方形结构体里。

但人们逐渐发现一些函数也是与这个结构体有关的,比如可以对这个长方体求周长或面积,所以可以把这些函数与这个结构体封装在一起,结构体中包含了数据,函数描述了行为,这种数据和行为的封装被称为对象

而为了能够便于多次创建同一类对象,被设计了出来,将对象内部的数据属性和行为抽离出来合到一起,这就成为了一个模板,由这个模板产生的对象就是实例

随着时间的推移,人们又发现一些不同的类之间也存在关系,并且是一种从属关系,比如说不管长方形三角形他们都是形状,不论是设计师还是程序员他们都是员工,老师和学生都是人,他们各有一些共性,如果把这些共性提取出来,就可以构成一些基础类,那就又可以提高复用能力了,事实上在不断的实践中人们也确实发现那些越是抽象和宽泛的类,其中的方法与有可能被复用,于是这种基于类的继承思想逐渐成型了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Animal {
private String name;
private double height;
private double weight;
public Animal(String name, double height, double weight) {
this.name = name;
this.height = height;
this.weight = weight;
}
public void walk() {
// walk
}
}

public class Dog extends Animal {
// ...
@Override
public void walk() {
// dog walk
}
}

public class Cat extends Animal {
// ...
@Override
public void walk() {
// cat walk
}
}

flowchart LR;
class1["class"] --> class2["class"];

所以说类与对象之间的关系就好比产线与产品的关系,方法和数据的描述通常在类中定义,而数据的值则通常在实例中赋予,所以可以说基于类的继承实际上继承了数据的结构与模板的方法,而没有继承数据,但接下来要说的原型继承则能够连数据也一并继承。

原型继承

在类继承中,对象的模板是类,而在原型继承中,对象的模板是原型。其实我觉得在面向对象思想的术语中,“原型”这个名字相较于“类”更加直观,原型更加贴近模板的含义,像是原型车、原型机,代表一种范式,而由这种范式则可以衍生出各式各样的特例。

那么,原型是什么?

首先,原型,也就是prototype,它的工作是给其他对象提供共享属性

对于函数而言,原型是一个属性。在JavaScript中,每个函数都有一个prototype属性,这个原型属性指向一个对象,这个对象称为原型对象。对于这个原型对象,不仅是函数的原型属性指向它,每个对象都有一个[[Prototype]]属性,这个属性也指向原型对象。

但实际上很多时候我们并不严格区分原型、prototype[[Prototype]],原型属性、原型对象,而是统称为原型。

当我们谈论的“原型”是原型对象时,事情离“继承”这个概念就近了一步。刚刚提到,每个对象都有一个[[Prototype]]属性,这个属性指向原型对象,而这个原型对象作为一个对象,它也有自己的原型对象,如此套娃就形成了一条原型链

flowchart LR;
object1["object"] --> object2["object"];

所以与类继承不同,原型继承中继承的并非类,而是对象,这就使原型继承拥有了继承数据的能力,所以有开发者认为能够继承对象的原型继承是比类继承更加面向对象的。

同时这也解释了当通过构造函数来实现继承时为什么会写出child.__proto__ = parentFunction.prototypeObject.setPrototypeOf(child, parentFunction.prototype) —— 把父对象构造函数的prototype赋值给子对象的[[Prototype]],因为对于构造函数而言,它的prototype属性指向它的原型对象,所以只有当子对象的原型属性指向这个对象时,这个原型继承链才完美形成。

JavaScript中的继承

现代JavaScript中也存在类的概念,不过JavaScript正统采用的是原型继承的思想,而类的概念是在ES2015实现的。

但是JavaScript中的类底层也是通过原型来模拟的,甚至为了让类看起来更像样,原型继承数据的能力被无情屏蔽了,这就是作为Java开发者在学习原型链时感到不得要领而学习类时则轻车熟路的原因了,无论在类中还是在类的构造函数中声明数据,它们都一律会出现在new出来的实例对象上,而非原型对象。

参考

深入理解 JavaScript 原型


浅谈原型继承和类继承
https://skycurtain.github.io/2022/09/21/prototypal-inheritance-and-classical-inheritance/
作者
Skycurtain
发布于
2022年9月21日
许可协议