基于原型编程

基于原型编程(英語:prototype-based programming)或称为原型程序设计原型编程,是面向对象编程的一种风格和方式。在原型编程中,行为重用(在基于类的语言通常称为继承),是通过复制已经存在的原型对象的过程实现的。这个模型一般被认为是无类的、面向原型、或者是基于实例的编程。

原型编程最初的(也是最经典的)例子是编程语言Self,它是由David Ungar英语David Ungar和Randall Smith开发的。但是无类编程方式最近变得越来越受欢迎,并且被JavaScriptCecil英语Cecil (programming language)NewtonScriptIoREBOL,还有一些其他的程序语言所采纳。

与基于类编程的比较

基于类编程当中,对象总共有两种类型:接口。类包含存储数据的结构和操纵数据的行为,结构是用数据字段描述的,而行为是通过方法定义的。接口是不包含字段的抽象类型,通常定义类必须实现的行为,接口不能实例化而必须被实现。所有的类通过提供结构和行为来实现一个接口。类可以从现存的类继承而来,从而建立一种类层级。

原型编程的主张者经常争论说,基于类的语言提倡使用一个关注分类和类之间关系的开发模型。与此相对,原型编程看起来提倡,程序员关注一系列对象实例的行为,而之后才关心如何将这些对象划分到最近的使用方式相似的原型对象,而不是分成类。因为如此,很多基于原型的系统提倡运行时原型的修改,而只有极少数基于类的物件導向系统(比如第一个动态物件導向的系统Smalltalk),允许类在程序运行时被修改。

考虑到绝大多数基于原型的系统,是基于解释型的和动态类型程序语言,这里要重点指出的是,静态类型语言实现基于原型从技术上是可行的。用基于原型编程描述的Omega语言[1],就是这样系统的一个例子。尽管根据Omega网站所述,Omega也不是完全的静态,但是可能的时候,它的编译器有时会使用静态绑定来改进程序的效率。

对象构造

在基于类的语言中,一个新的实例通过类构造器和给构造器的可选的参数来构造。在基于原型的语言中,没有显式的类,对象直接通过一个原型属性从其他对象进行继承,这个原型属性,在JavaScript中叫做prototype,在Io中叫做proto。在基于原型的系统中,构造对象有两种方法,通过复制(cloning)已有的对象,或者通过扩展空(nihilo)对象创建,因为大多数系统提供了不同的复制方法,扩展空对象的方式并不显著[2]

提供扩展空对象创建的系统允许对象从空白中创建,而无需从已有的原型中复制。这样的系统提供特殊的文法,用以指定新对象的行为和属性,无须参考已存在的对象。在很多原型语言中,通常有一个Object原型,其中有普遍需要的方法。它被用作所有其它对象的最终原型。扩展空对象创建可以保证新对象不会被顶级对象的命名空间污染。例如在JavaScript中,可以利用null原型來做到,比如Object.create(null)

复制指一个新对象通过复制一个已经存在的对象(就是他的原型)来构造自己的过程。于是新的对象拥有原来对象的所有属性,从这一点出发新对象的属性可以被修改。在某些系统中,子对象持有一个到它原型的直接链接(经由授权或类似方式)。并且原型的改变同样会导致它的副本的变化。其他系统中,如类Forth的程序语言Kevo,在此情况下不传播原型的改变,而遵循一个更加连续的模型,其中被复制的对象改变不会通过他的副本传播[3]

// JavaScript中真实的原型继承样式的例子。 // 使用文字对象记号{}建立的“无中生有”对象。var foo = {name: "foo", one: 1, two: 2};// 另一个“无中生有”对象。var bar = {three: 3};// Gecko和Webkit JavaScript引擎可以直接的操纵内部的原型链接。// 为了简单起见,我们假装下面几行代码可以工作而不考虑使用的引擎:bar.__proto__ = foo; // foo现在是bar的原型。// 如果我们尝试从bar访问foo的属性,从此以后会成功。 bar.one // 解析为1。// 子对象的属性也是可访问的。bar.three // 解析为3。// 自身的属性遮蔽原型属性。bar.name = "bar";foo.name; // 无影响,解析为"foo"。bar.name; // 解析为"bar"。

下面是个在 JavaScript 1.8.5 以上版本的例子(参见ECMAScript 5兼容性表格[4]

var foo = {one: 1, two: 2};// 等价于上例的bar.[[ prototype ]] = foovar bar = Object.create( foo );bar.three = 3;bar.one; // 1bar.two; // 2bar.three; // 3

委托

在使用委托的基于原型的语言中,语言运行时能够分派正确的方法或找到正确的数据,只需遵循一系列委托指针(从对象到其原型)直到找到匹配项。在对象之间建立这种行为共享所需的只是委托指针。与基于类的面向对象语言中的类和实例之间的关系不同,原型与其分支之间的关系不要求子对象在此链接之外与原型具有内存或结构相似性。因此,子对象可以随着时间的推移继续被修改和修正,而无需像在基于类的系统中那样重新安排其相关原型的结构。要注意,同样重要的是,不仅可以添加或更改数据,还可以添加或更改方法。出于这个原因,一些基于原型的语言将数据和方法都称为“槽”(slot)或“成员”。

串接

串接原型(Kevo 编程语言实现的方法)中,没有可见的指针或链接指向克隆对象的最初原型。原型(父)对象被复制而不是链接到,并且没有委托。因此,对原型的更改不会反映在克隆对象中[5]

这种安排下的主要概念差异是对原型对象所做的更改不会自动传播到克隆。这可能被视为优点或缺点(然而,Kevo 确实提供了额外的原语,用于基于对象的相似性——所谓的家族相似性或克隆家族机制——而不是像委托模型中典型的那样通过分类起源发布更改)。有时还声称基于委托的原型设计还有一个缺点,即对子对象的更改可能会影响父对象的后续操作。然而,这个问题不是基于委托的模型所固有的,也不存在于基于委托的语言(如 JavaScript)中,它确保对子对象的更改总是记录在子对象本身中,而不会记录在父对象中(即子对象的value 会影响父级的值,而不是更改父级的值)。

这样做的好处包括,对象的作者可以修改这份副本,而无须担心对此父类的其他子类产生副作用。进一步的优点,是查找属性运算的消耗同授权相比大大降低了,授权查找必须遍历整个委托链才能判定不存在。

串接的坏处包括传播变化到整个系统的难度;如果一个变化作用到某个原型,它不会立即或者自动的对它的所有副本生效。然而Kevo提供了额外的在对象系统中传播变化的方式。这种方式是基于他们的相似性(所谓的family相似)[5],而非像委托模型具有代表性的那样源自分类学。

另外一个坏处是在这个模型的大多数自然的实现下,每一个副本上都有额外的内存被浪费掉了(相对委托模型而言),因为副本和原型之间有相同的部分存在。然而,在共享的实现和后台数据中提供串接行为的编程编程是可行的。这种做法为Kevo所遵从[6]

批评

那些经常批评基于原型系统而支持基于类的对象模型的人,通常有类似静态类型系统相对于动态类型系统的担心。通常这些担心是:正确性、安全性、可预测性以及效率。

在前三点上,类可以看作和类型等效(多数静态语言遵守此规则),而且提供保证他们实例的契约,而对这些实例的使用者保证特定场景中的行为。

在最后一点上,效率,类的声明简化了编译器的组织,允许开发高效的方法以及实例变量查找。对Self语言来说,大多开发时间都消耗在开发、编译以及解释技术,用以改进基于原型的系统相对于基于类的系统的性能。举例来说Lisaac产生的代码速度几乎跟C一样快。测试是由MPEG-2编码器的Lisaac版本得出的,它由一个C语言版本复制而来。测试显示,Lisaac版本比C版本慢1.9%,但代码行数少了37%。然而C语言并非物件導向语言,而是一个过程式语言。Lisaac跟C++版本相比可能更说明问题。

最普遍的对基于原型的语言的批评,来自不喜欢它的软件开发者社区,僅管JavaScript有着人气和市场。对基于原型系统的了解程度,似乎因为JavaScript框架的廣泛應用,以及JavaScript针对web 2.0的复杂应用而改变[7]。很可能由于这些原因,在ECMAScript标准的第四版开始,尋求使JavaScript提供基于类的构造,且ECMAScript第六版,提供“類”作為原有的原型架構之上的語法糖,提供建構物件與處理繼承時的另一種語法[8]

支持基于原型编程的语言

参见

引用

參考來源

  • James Noble (ed.), Antero Taivalsaari (ed.), Ivan Moore (ed.) (编). Prototype-Based Programming: Concepts, Languages and Applications. Springer-Verlag. 1999. ISBN 981-4021-25-3. 
  • Abadi, Martin; Luca Cardelli. A Theory of Objects. Springer-Verlag. ISBN 0-387-94775-2. 

外部链接