zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Yet Another OOP : 基于原型而非基于类

2023-03-15 22:02:25 时间

OOP是一种程序设计的范式,是设计思想,在多种现代编程语言中都提供语法支持。然而,OOP就只是我们所知道的封装继承多态吗? 套用OOP的说法,OOP的设计思想是抽象,而OOP的编程语法只是实现。

OOP

前阵子在知乎回答了一个问题,题主认为C++的三大特性就是封装继承多态,且不说C++本身支持多种范式,即便是OOP也绝对不仅仅局限于这三个特征。

例如继承并不能唯一表示类之间的关系,只能表示is-a,还有组合(composition),委托(delegation),具体可以参考《重构》那本书,里面会把很多不恰当的继承通过重构修改成组合或者委托,这也是很多设计模式的思想。

例如c++的多态只支持dynamic dispatch(虚函数),而不支持multiple dispatch。举个例子,钥匙开锁,这里的钥匙和锁的类型理论上都能影响到开锁流程,但是c++在绑定代码时只会看一个类型。(钥匙可以是密码、电子钥匙、机械钥匙、瞳孔识别、人脸识别,锁可以分为密码锁、机械锁、手机锁屏等等)

OOP的创始人,第一个OOP语言Smalltalk的设计者Alan Kay这么说

I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages (so messaging came at the very beginning -- it took a while to see how to do messaging in a programming language efficiently enough to be useful) OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things. It can be done in Smalltalk and in LISP. OOP应该体现一种网状结构,这个结构上的每个节点只能通过“消息”和其他节点通讯。它维持、保护、并隐藏着内部状态,延迟绑定一切,并且仅能通过message修改其他节点的状态。

可见封装继承多态,只是编程语言对一种OOP的实现罢了,就连公认的函数式编程FP语言Lisp都能实现OOP。除了常见的基于类的实现(class-based),另一种实现则基于原型(prototype-based),而他最大的发扬者就是使用率很高的一门语言JavaScript,和虽然我没接触但是早有耳闻的原型链

作为以C++/JAVA/Python/R为技术栈的后端程序员,我对JavaScript本身并不了解,曾经前端编程也是用TypeScript来写,它在JavaScript的基础上实现了一套后端更熟悉的class-based的语法,可编译成JavaScript,后来JavaScript自身也支持了class关键字,但是其实现仍然是prototype-based。

借着这次回答,了解一下prototype-based的OOP,也分享给其他没接触前端的同学。


Prototype-based OOP

原型思想如果学过设计模式就应该能知道,它是创建型设计模式的一种,强调新对象的产生必须通过原型clone得到。

在基于原型的系统中构造对象有两种方法,通过复制(clone)已有的对象,或者通过扩展空对象创建。很多框架例如Vue都会通过扩展空对象的方式自己重写一套原型链。

例如,在JavaScript中,默认的顶级对象是Object,它自身具备一些通用的属性和方法,被原型链上的所有对象继承。对象也可以重写继承来的属性(实际上,就相当于对象内部有个指针指向原型对象)。

  1. 通过Object.create(proto,[propertiesObject]) 可以clone Object,并且增加新的属性。某些语法例如定义对象实质上就是进行上面的操作。
  2. 通过Object.create(null)、Object.create(undefined),可以扩展空对象
  3. 通过Object.setPrototypeOf(obj, proto)这种语法糖也可以动态修改obj的原型

Object是a的原型,a是b的原型,这就是简单的原型链

对于C++而言,子类内联了父类的所有属性,修改派生类对象的属性就是修改基类对象的属性。对象的结构是静态的。

对于JavaScript而言,则是优先查找当前对象,然后沿着原型链查找原型对象上是否有该属性,修改当前对象的属性并不影响到原型对象的属性,而修改原型对象的属性则可能会影响到当前对象。对象的结构是动态的。

修改b的原型a,影响了b的属性

这个实现有些类似于之前提到的委托(delegation),通过引用原型对象的方式来进行复用。

某些语言例如wiki中说的Kevo,则是通过副本的方式,直接拷贝原型的所有属性,修改原型对象不会影响到当前对象,同时也不需要遍历整个链来查找属性。但是修改原型对象不会影响到当前对象在传播方面也有坏处,有时候我们就想影响,比如修改函数实现,因此需要其他传播机制;同时,这也是经典的时空交换,空间占用更大了。


Revisit OOP

原型这种纯粹基于对象的实现,反倒更像是字面意义上的OOP,毕竟人家OOP只说了有Object,没说有Class。

回头想一想,基于原型的实现是否满足OOP思想呢?

  • 函数和对象绑定,而不是和类绑定,天生就实现了dynamic dispatch。就像是C++为了实现这个功能,也要将vptr和对象绑定一样。
  • 沿着原型链查找属性和方法,同样实现了代码复用,复制自同一原型的对象,也具有相同的子结构。就像是C++的继承,复用了基类的数据和方法一样。
  • 封装,则是构造函数相关,和原型倒没啥关系,主要是通过函数式编程的闭包机制。C++的封装,则是通过权限机制,只允许对应的函数访问。