我正在参与「掘金启航方案」
承继(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。三种言语各有其完结特点,但中心思维基本是共同的,即直接在目标之间建立引证联系,然后便于进行办法和特点的查找。
参考
- 依据原型的承继形式
- 《JavaScript 高档程序规划》
- 《JavaScript 言语精粹》
- 《七周七言语:理解多种编程范式》
- prototype —— Prototype Based OO Programming For Lua
- Javascript承继机制的规划思维
- Prototype-based programming
- Difference from class-based inheritance
- What are advantanges and disadvantages of prototypal OOP
- What is prototype-based OOP?
- Prototype chains and classes
- lua-object
- 01.原型(prototype)和原型链(prototype chain)
- 目标原型
- JavaScript’s Pseudo Classical Inheritance diagram
- Programming in Luauu