原型编程最初的(也是最经典的)例子是编程语言 Self,它是由 David Ungar 和 Randall Smith 开发的。但是 classless 编程方式最近变得越来越受欢迎,并且被 JavaScript、Squeak (当使用观察者框架操作 Morphic 组件时)、Cecil、NewtonScript、Io、MOO、REBOL 还有一些其他的程序语言所采纳。
与基于类的模型的比较
在基于类的编程当中,对象总共有两种类型。类定义了对象的基本布局和函数特性,而接口是“可以使用的”对象,它基于特定类的样式。在此模型中,类表现为行为和结构的集合,就接口持有对象的数据而言,对所有接口来说是相同的。区分规则因而首先是基于结构和行为,而后是状态。原型编程的主张者经常争论说基于类的语言提倡使用一个关注分类和类之间关系开发模型。与此相对,原型编程看起来提倡程序员关注一系列对象实例的行为,而之后才关心如何将这些对象划分到最近的使用方式相似的原型对象,而不是分成类。因为如此,很多基于原型的系统提倡运行时原型的修改,而只有极少数基于类的面向对象系统(比如第一个动态面向对象的系统 Smalltalk)允许类在程序运行时被修改。
原型编程通常穿插在教授 cognitive psychology 的课程中,他强调 prototypes 和 exemplars 作为学习中的关键词。
考虑到绝大多数基于原型的系统是基于解释性的和动态类型程序语言,这里要重点指出的是静态类型语言实现基于原型从技术上是可行的。用基于原型编程描述的 Omega 语言就是这样系统的一个例子。尽管根据 Omega 网站所述,Omega 也不是完全的静态,但是可能的时候,它的编译器有时会使用静态绑定来改进程序的效率。
对象构造
在基于类的语言中,一个新的实例通过类构造器和构造器可选的参数来构造,结果实例由类选定的行为和布局建立模型。在基于原型的系统中构造对象有两种方法,通过复制已有的对象 或者通过扩展 nihilo(空的)对象创建,因为大多数系统提供了不同的复制方法,扩展 nihilo 对象的方式鲜为人知。
提供扩展 nihilo 对象创建的系统允许对象从空白中创建而无需从已有的原型中复制。这样的系统提供特殊的文法用以指定新对象的行为和属性,无须参考已存在的对象。在很多原型语言中,通常有一个 Object 原型,其中有普遍需要的方法。它被用作所有其它对象的最终原型。扩展 nihilo 对象创建可以保证新对象不会被顶级对象的命名空间污染。(在 Mozilla 的 JavaScript 实现中,可以通过设置一个新创建对象的 __proto__ 属性为 null 来做到)。
Cloning 指一个新对象通过复制一个已经存在的对象(就是他的原型)来构造自己的过程。于是新的对象拥有原来对象的所有属性,从这一点出发新对象的属性可以被修改。在某些系统中,子对象持有一个到它原型的直接链接(经由授权或类似方式)。并且原型的改变同样会导致它的副本的变化。其他系统中,如类 Forth 的程序语言,Kevo 在此情况下不传播原型的改变,而遵循一个更加连续的模型,其中被复制的对象改变不会通过他的副本传播。
// Example of true prototypal inheritance style // in JavaScript. // "ex nihilo" object creation using the literal // object notation {}. var foo = {name: "foo", one: 1, two: 2}; // Another "ex nihilo" object. var bar = {three: 3}; // Gecko and Webkit JavaScript engines can directly // manipulate the internal prototype link. // For the sake of simplicity, let us pretend // that the following line works regardless of the // engine used: bar.__proto__ = foo; // foo is now the prototype of bar. // If we try to access foo's properties from bar // from now on, we'll succeed. bar.one // Resolves to 1. // The child object's properties are also accessible. bar.three // Resolves to 3. // Own properties shadow prototype properties bar.name = "bar"; foo.name; // unaffected, resolves to "foo" bar.name; // Resolves to "bar"
下面是个在 JavaScript 1.8.5 以上版本的例子(参见 http://kangax.github.com/es5-compat-table/ )
var foo = {one: 1, two: 2}; // bar.[[ prototype ]] = foo var bar = Object.create( foo ); bar.three = 3; bar.one; // 1 bar.two; // 2 bar.three; // 3
委托
在使用委托的基于原型的语言中,运行时语言可以仅仅通过循着一个序列的指针直到找到匹配这样的方式来定位属性或者寻找寻找正确的数据。所有这些建立行为共享的行为 需要的是委托指针。不像是基于类的面向对象语言中类和接口的关系,原型和他的分支之间的关系并不要求子对象有相似的内存结构,因为如此,子对象可以继续修改和...而无须像基于类的系统那样整理结构。还有一个要提到的地方是,不仅仅是数据,方法也能被修改。因为这个原因,大多数基于类型的语言把数据和方法提作“slots”。
关联
在纯粹的原型,又被称作 concatenative 原型(以 Kevo 语言为例)中没有到被复制的原型对象的指针或链接。原型对象以重新给定名字(或引用)的方式被确实的复制了。这个过程类似于生物学上的分裂,属性和方法被原样复制。
这样做的好处包括对象的作者可以修改这份副本而无须担心对此父类的其他子类产生副作用。进一步的优点是查找属性运算的消耗同授权相比大大降低了,授权查找必须遍历整个委托链才能判定不存在。
Concatenative 的坏处包括传播变化到整个系统的难度;如果一个变化作用到某个原型,它不会立即或者自动的对它的所有副本生效。然而Kevo提供了额外的在对象系统中传播变化的方式。这种方式是基于他们的相似性(所谓的 family 相似)而非像委托模型具有代表性的那样源自分类学。
另外一个坏处是在这个模型的大多数自然的实现下,每一个副本上都有额外的内存被浪费掉了(相对委托模型而言),因为副本和原型之间有相同的部分存在。然而,在共享的实现和后台数据中提供 concatenative 行为的编程编程是可行的。这种做法为 Kevo 所遵从。
批评
那些经常批评基于原型系统而支持基于类的对象模型的人通常有类似静态类型系统相对于动态类型系统的担心。通常这些担心是:正确性、安全性、可预测性以及效率。
在前三点上,类可以看作和类型等效(多数静态语言遵守此规则)而且提供保证他们实例的契约,而对这些实例的使用者保证特定场景中的行为。
在最后一点上,效率,类的声明简化了编译器的组织,允许开发高效的方法以及实例变量查找。对Self语言来说,大多开发时间都消耗在开发、编译以及解释技术用以改进基于原型的系统相对于基于类的系统。举例来说 Lisaac 产生的代码速度几乎跟C一样快。测试是由 MPEG-2 编码器的 Lisaac 版本得出的,它由一个C语言版本复制而来。测试显示,Lisaac 版本比 C 版本慢1.9%,但代码行数少了37%。然而C语言并非面向对象语言,而是一个过程式语言。Lisaac 跟 C++ 版本相比可能更说明问题。
最普遍的对基于原型的语言的批评来自不喜欢它的软件开发者社区,尽管有 JavaScript 的人气和市场。对基于原型系统的了解程度似乎因为JavaScript 框架的广泛应用以及 JavaScript 针对 Web 2.0的复杂应用而改变。很可能由于这些原因,在 ECMAScript 标准的第四版开始寻求令 JavaScript 提供基于类的构造。根据标准定义来看,它毫无用处。用传统的基于类的语法构造的对象仍然存在原型。