基于原型(Prototype)的编程其实也是面向对象编程的一种方式。没有 class 化的,直接使用对象。又叫基于实例的编程。其主流的语言就是 JavaScript,与传统的面向对象编程的比较如下:
- 在基于类的编程当中,对象总共有两种类型。类定义了对象的基本布局和函数特性,而接口是“可以使用的”对象,它基于特定类的样式。在此模型中,类表现为行为和结构的集合,对所有接口来说这些类的行为和结构都是相同的。因而,区分规则首先是基于行为和结构,而后才是状态。
- 原型编程的主张者经常争论说,基于类的语言提倡使用一个关注分类和类之间关系的开发模型。与此相对,原型编程看起来提倡程序员关注一系列对象实例的行为,而之后才关心如何将这些对象划分到最近的使用方式相似的原型对象,而不是分成类。

正因如此,很多基于原型的系统提倡运行时进行原型的修改,而只有极少数基于类的面向对象系统(比如第一个动态面向对象的系统 Smalltalk)允许类在程序运行时被修改。
- 在基于类的语言中,一个新的实例通过类构造器和构造器可选的参数来构造,结果实例由类选定的行为和布局创建模型。
- 在基于原型的系统中构造对象有两种方法,通过复制已有的对象或者通过扩展空对象创建。很多基于原型的系统提倡运行时进行原型的修改,而基于类的面向对象系统只有动态语言允许类在运行时被修改(Python、Ruby...)
1. Object
Object:对象是一个属性的集合,且有一个唯一的原型。原型可以是一个对象或空值。
let point = {
x: 10,
y: 20,
}

对于上面定义的 point 对象,proto 属性指向 point 对象的原型。原型对象用于通过动态分派机制实现继承。下面让我们用原型链的概念来详细了解这种机制。
2. Prototype
Protype:一个 prototype 是一个委托对象,用于实现基于原型的继承。
每个对象在创建时都会接收到其原型。如果未明确设置原型,对象将接收 Object.prototype 作为其原型。可以通过 proto 属性或 Object.create()
方法显式设置原型:
// Base object.
let point = {
x: 10,
y: 20,
}
// Inherit from `point` object.
let point3D = {
z: 30,
__proto__: point,
}
console.log(
point3D.x, // 10, inherited
point3D.y, // 20, inherited
point3D.z // 30, own
)
Prototype chain:原型链是用于实现继承和共享属性的有限对象链。
任何对象都可以作为另一个对象的原型,而原型本身可以有自己的原型。如果一个原型还有原型,以此类推,它被称为原型链。

规则很简单:如果在对象本身中找不到某个属性,则尝试在原型中找、在原型的原型中找,以此类推直到找遍整个原型链,如果最终在原型链中找不到属性,则返回 undefined。从技术上讲,这种机制称为动态调度或委托。
委托:一种用于解析继承链中的属性的机制。该过程发生在运行时,因此也称为动态调度。
💡 与在编译时解析引用时的静态分派相比,动态分派在运行时解析引用。
对象字面量实际上永远不会为空,即它总是将 Object.prototype 作为其默认原型,并继承其属性。若要创建无原型的对象,必须将其原型显式设置为 null:
// Doesn't inherit from anything.
let dict = Object.create(null)
console.log(dict.toString) // undefined
动态分派机制允许继承链的灵活可变性,提供更改委托对象的能力:
let protoA = { x: 10 }
let protoB = { x: 20 }
// Same as `let objectC = {__proto__: protoA};`:
let objectC = Object.create(protoA)
console.log(objectC.x) // 10
// Change the delegate:
Object.setPrototypeOf(objectC, protoB)
console.log(objectC.x) // 20
INFO
尽管 proto 属性在今天已经标准化,并且更容易解释,但在实践中更喜欢使用 API 方法进行原型操作,如 Object.create、Object.getPrototypeOf、 Object.setPrototypeOf、通过 Reflect 模块操作对象。
很多面向对象语言都是基于类的概念实现继承。ECMAScript 2015 中也实现了这种基于类(class)的抽象继承方式,我们来看看其隐藏的实现的细节。
3. Class
Class:类是一个正式的抽象集,它指定其对象的初始状态和行为。
当几个对象共享相同的初始状态和行为时,它们就形成了一个分类。如果我们需要从同一个原型继承多个对象,可以创建这个原型,并从新创建的对象中显式继承它:
// Generic prototype for all letters.
let letter = {
getNumber() {
return this.number
},
}
let a = { number: 1, __proto__: letter }
let b = { number: 2, __proto__: letter }
let z = { number: 26, __proto__: letter }
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber() // 26
)

但是,这样写显然很麻烦。而类抽象正是为了解决这个问题,作为一个语法糖(即一个在语义上做同样的事情,但语法形式更好的结构),它允许使用一种更便捷的方式创建这样的多个对象:
class Letter {
constructor(number) {
this.number = number
}
getNumber() {
return this.number
}
}
let a = new Letter(1)
let b = new Letter(2)
let z = new Letter(26)
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber() // 26
)
💡 ECMAScript 中基于类的继承是在基于原型的委托实现的。“类”只是一个理论抽象。从技术上讲,它可以使用 Java 或 C++ 中的静态调度来实现,也可以使用 JavaScript、Python、Ruby 等中的动态调度(委托)来实现。
从技术上来看,class 其实就是“构造函数 + 原型”的组合。构造函数创建对象,并自动为其新创建的实例设置原型。
Constructor:构造函数是用于创建实例并自动设置其原型的函数。
可以显式使用构造函数。此外,在引入 class 语法糖之前,JS 开发人员过去没有更好的选择(仍然可以看到很多这样的遗留代码):
function Letter(number) {
this.number = number
}
Letter.prototype.getNumber = function () {
return this.number
}
let a = new Letter(1)
let b = new Letter(2)
let z = new Letter(26)
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber() // 26
)
虽然这样单级的继承使用构造函数非常简单,但如果父类很多代码就很容易变得杂乱。class 语法糖正是隐藏了这些细节。
我们来看看上面构造函数例子中的完整关系图:

上图显示每个对象都有一个原型。甚至构造函数(类)Letter 都有自己的原型,即 Function.prototype。构造函数的 prototype 属性只是对将要构造实例的原型的引用。其次构造函数也是对象(函数对象),其__proto__
属性指向 Object.prototype ,再补充一张简化继承关系图:
