基于原型(Prototype)的编程其实也是面向对象编程的一种方式。没有class化的,直接使用对象。又叫,基于实例的编程。其主流的语言就是JavaScript,与传统的面对象编程的比较如下:

因为如此,很多基于原型的系统提倡运行时进行原型的修改,而只有极少数基于类的面向对象系统(比如第一个动态面向对象的系统Smalltalk)允许类在程序运行时被修改。

JavaScript的原型概念

这里,我们主要以JavaScript举例,面向对象里面要有个Class。但是JavaScript觉得不是这样的,它就是要基于原型编程,就不要Class,就直接在对象上改就行了,基于编程的修改,直接对类型进行修改。

我们先来看一个示例。

var foo = {name: "foo", one: 1, two: 2};

var bar = {three: 3};

每个对象都有一个 __proto__ 的属性,这个就是“原型”。对于上面的两个对象,如果我们把 foo 赋值给 bar.__proto__,那就意味着,bar 的原型就成了 foo的。

bar.__proto__ = foo; // foo is now the prototype of bar.

于是,我们就可以在 bar 里面访问 foo 的属性了。

// 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的两个东西,一个是 __proto__,另一个是 prototype,这两个东西很容易混淆。这里说明一下:

在JavaScript中,对象有两种表现形式, 一种是 Object (ES5关于Object的文档),一种是 FunctionES5关于Function的文档)。

我们可以简单地认为,__proto__ 是所有对象用于链接原型的一个指针,而 prototype 则是 Function 对象的属性,其主要是用来当需要new一个对象时让 __proto__ 指针所指向的地方。 对于超级对象 Function 而言, Function.__proto__ 就是 Function.prototype

比如我们有如下的代码:

var a = {
  x: 10,
  calculate: function (z) {
    return this.x + this.y + z;
  }
};
 
var b = {
  y: 20,
  __proto__: a
};
 
var c = {
  y: 30,
  __proto__: a
};
 
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80

其中的“原型链”如下所示:

注意:ES5 中,规定原型继承需要使用 Object.create() 函数。如下所示:

var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});

好了,我们再来看一段代码:

// 一种构造函数写法
function Foo(y) {
  this.y = y;
}
 
// 修改 Foo 的 prototype,加入一个成员变量 x
Foo.prototype.x = 10;
 
// 修改 Foo 的 prototype,加入一个成员函数 calculate
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};
 
// 现在,我们用 Foo 这个原型来创建 b 和 c
var b = new Foo(20);
var c = new Foo(30);
 
// 调用原型中的方法,可以得到正确的值
b.calculate(30); // 60
c.calculate(40); // 80

那么,在内存中的布局是怎么样的呢?大概是下面这个样子。

这个图应该可以让你很好地看明白 __proto__prototype 的差别了。

我们可以测试一下:

b.__proto__ === Foo.prototype, // true
c.__proto__ === Foo.prototype, // true
 
b.constructor === Foo, // true
c.constructor === Foo, // true
Foo.prototype.constructor === Foo, // true
 
b.calculate === b.__proto__.calculate, // true
b.__proto__.calculate === Foo.prototype.calculate // true

这里需要说明的是:

Foo.prototype 自动创建了一个属性 constructor,这是一个指向函数自己的一个reference。这样一来,对于实例 bc 来说,就能访问到这个继承的 constructor 了。

有了这些基本概念,我们就可以讲一下JavaScript的面向对象编程了。

注: 上面示例和图示来源于 JavaScript, The Core 一文。

JavaScript原型编程的面向对象

我们再来重温一下上面讲述的内容:

function Person(){}
var p = new Person();

Person.prototype.name = "Hao Chen";
Person.prototype.sayHello = function(){
    console.log("Hi, I am " + this.name);
}

console.log(p.name); // "Hao Chen"
p.sayHello(); // "Hi, I am Hao Chen"

在上面这个例子中:

注意一下:

好了,我们再来看一下“原型编程”中面向对象的编程玩法。

首先,我们定义一个 Person 类。

//Define human class
var Person = function (fullName, email) {
  this.fullName = fullName;
  this.email = email;
  
  this.speak = function(){
    console.log("I speak English!");
  };
  this.introduction = function(){
    console.log("Hi, I am " + this.fullName);
  };
}

上面这个对象中,包含了:

其实,所谓的方法也是属性。

然后,我们可以定义一个 Student 对象。

//Define Student class
var Student = function(fullName, email, school, courses) {

  Person.call(this, fullName, email);

  // Initialize our Student properties
  this.school = school;
  this.courses = courses;
  
  // override the "introduction" method
  this.introduction= function(){
	console.log("Hi, I am " + this.fullName + 
				". I am a student of " + this.school + 
				", I study "+ this.courses +".");
  };
  
  // Add a "exams" method
  this.takeExams = function(){
    console.log("This is my exams time!");
  };
};

在上面的代码中:

虽然,我们这样定义了 Student,但是它还没有和 Person 发生继承关系。为了要让它们发生关系,我们就需要修改 Student 的原型。

我们可以简单粗暴地做赋值:Student.__proto__ = Person.prototype ,但是,这太粗暴了。

我们还是使用比较规范的方式:

// Create a Student.prototype object that inherits 
// from Person.prototype.
Student.prototype = Object.create(Person.prototype); 

// Set the "constructor" property to refer to Student
Student.prototype.constructor = Student;

这样,我们就可以这样使用了。

var student = new Student("Hao Chen", 
						  "haoel@hotmail.com",
						  "XYZ University", 
						  "Computer Science");
student.introduction();   
student.speak();       
student.takeExams(); 

// Check that instanceof works correctly
console.log(student instanceof Person);  // true 
console.log(student instanceof Student); // true

上述就是基于原型的面向对象编程的玩法了。

注:在ECMAScript标准的第四版开始寻求使JavaScript提供基于类的构造,且ECMAScript第六版有提供"class"(类)作为原有的原型架构之上的语法糖,提供构建对象与处理继承时的另一种语法。

小结

我们可以看到,这种玩法就是一种委托的方式。在使用委托的基于原型的语言中,运行时语言可以“仅仅通过序列的指针找到匹配”这样的方式来定位属性或者寻找正确的数据。所有这些创建行为、共享的行为需要的是委托指针。

不像是基于类的面向对象语言中类和接口的关系,原型和它的分支之间的关系并不要求子对象有相似的内存结构,因为如此,子对象可以继续修改而无需像基于类的系统那样整理结构。还有一个要提到的地方是,不仅仅是数据,方法也能被修改。因为这个原因,大多数基于原型的语言把数据和方法提作“slots”。

这种在对象里面直接修改的玩法,虽然这个特性可以带来运行时的灵活性,我们可以在运行时修改一个prototype,给它增加甚至删除属性和方法。但是其带来了执行的不确定性,也有安全性的问题,而代码还变得不可预测,这有点黑科技的味道了。因为这些不像静态类型系统,没有一个不可变的契约对代码的确定性有保证,所以,需要使用者来自己保证。

以下是《编程范式游记》系列文章的目录,方便你了解这一系列内容的全貌。这一系列文章中代码量很大,很难用音频体现出来,所以没有录制音频,还望谅解。