我正在参与「掘金启航方案」

承继(Inheritance)是 面向目标编程(Object Oriented Programming, OOP)的三大特性之一,其他两大特性是 封装(Encapsulation)和 多态(Polymorphism)。在编程言语中,承继的主流完结办法有两种,分别是:

  • 依据类的承继(Class-based Inheritance):绝大多数面向目标编程言语都运用了依据类的承继,比方:C++、Java、Swift、Kotlin 等。
  • 依据原型的承继(Prototype-based Inheritance):少数面向目标编程言语运用依据原型的承继,一般都是解说型编程言语,即脚本言语,比方:JavaScript、Lua、Io、Self、NewtonScript 等。

除此之外,有一些辅佐承继的完结办法,比方:接口承继类型混入,一般用于完结多类型复用,能够达到相似多承继的效果。

本文,咱们来简略介绍一下其间依据原型的承继形式。

依据类的承继 vs 依据原型的承继

在依据类承继的言语中,目标是类的实例,类能够从另一个类承继。从本质上而言,类相当于模板,目标则经过这些模板来进行创立。

下图所示,为依据类的承继完结示意图。每个类都有一个相似 superclass指针指向其父类。每个目标都有一个相似 isa 的指针指向其所属的类。

此外,每个类都存储了一系列办法,可用于其实例进行查找和同享。关于办法存储办法,不同言语的完结有所不同。

  • 对于 C++ 等言语,每个类会保存一切先人类的办法地址。因而,在办法查找时,无需沿着承继链进行查找
  • 对于 Ruby、Objective-C 等言语,每个类只会保存其所界说的办法地址,而不保存先人类的办法地址。因而,在办法查找时,会沿着承继链进行查找,这种形式也被称为 音讯传递(Message Passing)。

基于原型的继承模式

在依据原型承继的言语中,没有类的概念,目标能够直接从另一目标承继。中心省掉了经过模板创立目标的进程。

下图所示,为依据原型的承继完结示意图。每个目标都有一个相似 prototype 的指针指向其原型目标。

每个目标存储了一系列办法,依据原型链,目标之间能够完结办法同享,当然也能够同享特点。办法和特点的查找进程,相似于上述的音讯传递,会沿着原型链进行查找。

基于原型的继承模式

原型承继的优缺陷

前面,咱们简略对比了两种承继形式的完结原理。下面,咱们来讨论一下原型承继的优缺陷。

对比而言,原型承继的长处主要有一下这些:

  • 避免许多的初始化工作。经过克隆一个现有目标来创立一个新目标,并具有相同的内部状况。
  • 具有十分强壮的动态性。经过修正原型链,能够将原型指针指向恣意目标,使得当前目标能够承继其他目标的才能。
  • 有用下降程序的代码量。因为原型承继没有类的概念,因而在代码完结中无需进行类的界说。

当然,凡事都具有两面性,以下罗列了一些原型承继的缺陷:

  • 功能开销相对较大。当咱们拜访特点或办法时,运转时会经过原型链进行查找,中心存在一个遍历的进程。
  • 原型同享的副作用。因为多个目标能够同享同一个原型目标,一旦某个目标修正原型目标的状况,将会对其他目标产生副作用。
  • 面向目标异类规划。绝大多数面向目标言语及教程都是依据类的完结而规划的,这对于习惯于依据类的 OOM 的开发者很简单产生困惑。

不同言语的原型承继完结

下面,咱们来看看不同编程言语中,依据原型的承继形式的完结细节。

JavaScript

JavaScript 原型完结存在着许多对立,它运用了一些杂乱的语法,使其看上去相似于依据类的言语,这些语法掩盖了其内在的原型机制。JavaScript 不直接让目标承继其他目标,而是供给了一个中心层——结构函数,完结目标的创立和原型的串联,然后间接完结目标承继。因为结构函数的界说相似于类界说,但又不是真正含义的类,因而咱们能够称之为 伪类(Pseudo Class)。

默认情况下,伪类包括一个 prototype 指针指向原型,目标包括一个 constructor 指针指向伪类(结构函数),两者之间的联系如下所示。

基于原型的继承模式

为了完结新的目标承继其他目标,一般会先修正伪类中 prototype 的指针,然后再调用伪类进行目标结构和原型绑定。如下所示,为一段代码实例。

function AType() {
    this.property = true;
}
AType.prototype.getSuperValue = function () {
    return this.property;
}
function BType() {
    this.subproperty = false;
}
// 承继 AType。即修正伪类 BType 的 prototype 指针,使其指向父目标。
BType.prototype = new AType();
BType.prototype.getSubValue = function () {
    return this.subproperty;
}
let instance = new BType();
console.log(instance.getSuperValue());  // true

其间 BType.prototype = new AType() 修正了 BType 伪类的 prototype 指针,使其指向 AType 目标。当咱们调用 BType 结构函数时,所结构的目标自动承继 AType 目标。如下所示,为依据原型的承继联系示意图,其间每个伪类的 prototype 指针都发生了变化,指向了其所承继的父目标。终究,生成的目标中会包括一个 __proto__ 指针指向父目标。依据 __proto__ 指针咱们能够构建一个完好的原型链。

基于原型的继承模式

当然,在原型承继形式中,原型链中的父目标可能会被多个子目标所同享,因而子目标之间的状况同步问题需求分外注意。一旦,某个子目标修正了父目标的状况,那么会一起影响其他子目标。关于怎么处理这个问题,JavaScript 中有许多处理办法,具体细节能够阅览相关书本和博客,这儿不作具体赘述。

Lua

Lua 中的 表(table) 是一种十分强壮且常用的数据结构,它相似于其他编程言语中的字典或哈希表,能够以键值对的办法存储数据,包括办法界说。通常会运用 table 来处理模块(module)、包(package)、目标(object)等相关完结。

与此一起,Lua 还供给了 元表(metatable) 的概念,其本质上依然是一个表结构。但是元表能够对表进行关联和扩展,允许咱们改动表的行为。

元表中最常用的键是 __index 元办法。当咱们经过键来拜访表时,假如对应的键没有界说值,那么 Lua 会查找表的元表中的 __index 键。假如 __index 指向一个表,那么 Lua 会在这个表中查找对应的键。

如下所示,咱们为表 a 设置一个元表,其间界说元表的。 __index 键为表 b。当查找表 a 时,对应的键没有界说,那么会去查找元表。判别元表是否界说了 __index 键,这儿界说为另一个表 b。所以,会在表 b 中查找对应的键。

setmetatable(a, { __index = b })

Lua 中的承继形式正是依据元表和 __index 元办法而完结的。如下所示,分别是 Lua 中承继形式的完结示意图,以及对应的代码完结。

基于原型的继承模式

RootType = { rootproperty = 0 }
function RootType:new (o) 
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end
SuperType = RootType:new({ superproperty = 0 })
SubType = SuperType:new({ subproperty = 0 })

RootType 是一个目标,其完结了一个 new 办法用于完结几项工作:

  • 结构目标,其本质上是一个表。
  • RootType 目标设置为新目标的元表。
  • RootType 目标的 __index 指向 RootType 目标本身。

终究构成图中所示的目标承继联系。因为 Lua 中的承继完结没有类的概念,而只有目标的概念。因而也被归类成依据原型的承继形式。当 SubType 目标中没有找到对应的键时,会依据 metatable 指针找到对应的元表,并依据元表的 __index 指针找到进一步查找的表目标 SuperType。假如 SuperType 中依然没有,那么持续依据 metatable__index 指针进行查找。

Io

Io 的承继形式也是依据原型完结的,它的完结相对而言更加简略、直观。

在 Io 中,一切都是目标(包括闭包、命名空间等),一切行为都是音讯(包括赋值操作)。这种音讯传递机制其实与 Objective-C、Ruby 是相同的机制。在 Io 中,目标的组成十分要害,其主要包括两个部分:

  • (slots):一系列键值对,能够存储办法或特点。
  • 原型(protos):一个内部的目标数组,记载该目标所承继的原型。

Io 运用克隆的办法创立目标,对应供给了一个 clone 办法。当对父目标进行克隆时,新目标的 protos 数组中会参加对父目标的引证,然后建立承继联系。如下所示,为 Io 中承继形式的完结示意图,以及对应的代码完结。

基于原型的继承模式

RootType := Object clone
RootType rootproperty := 0
SuperType := RootType clone
SuperType superproperty := 0
SubType := SuperType clone
SubType subproperty := 0

相比于 JavaScript 和 Lua 的链表式单承继形式,Io 是支撑多承继的,其采用了多叉树的形式来完结的,其间最要害的就是 protos 数组。很显然,protos 数组能够存储多个原型目标。因而,能够完结多承继。如下所示,是 Io 中多承继形式的完结示意图。

基于原型的继承模式

因而,Io 中办法和特点的查找办法也有所不同,其依据 protos 数组,运用深度优先查找的办法来进行查找。在这种形式下,假如一个目标承继的目标越多,那么办法和特点的查找效率也会越低。

总结

本文,咱们首先简略对比了依据类的承继形式与依据原型的承继形式,其中心区别在所以否依据类来进行构建承继联系。对于后者,没有类的概念,即便有,那也是一种语法糖,为了与依据类的言语挨近下降开发者的学习本钱和理解本钱。

其次,咱们简略介绍了依据原型承继的优缺陷。当咱们对编程言语进行技术选型时,也能够从这方面进行考虑和权衡,判别是否适用于特定的场景。

最后,咱们介绍了三种编程言语中依据原型的承继完结,分别是:JavaScript、Lua、Io。三种言语各有其完结特点,但中心思维基本是共同的,即直接在目标之间建立引证联系,然后便于进行办法和特点的查找。

参考

  1. 依据原型的承继形式
  2. 《JavaScript 高档程序规划》
  3. 《JavaScript 言语精粹》
  4. 《七周七言语:理解多种编程范式》
  5. prototype —— Prototype Based OO Programming For Lua
  6. Javascript承继机制的规划思维
  7. Prototype-based programming
  8. Difference from class-based inheritance
  9. What are advantanges and disadvantages of prototypal OOP
  10. What is prototype-based OOP?
  11. Prototype chains and classes
  12. lua-object
  13. 01.原型(prototype)和原型链(prototype chain)
  14. 目标原型
  15. JavaScript’s Pseudo Classical Inheritance diagram
  16. Programming in Luauu