引言
基于原型(prototype)的编程其实也是面向对象编程的一种方式。没有 class 化的,直接使用对象。又叫,基于实例的编程。其主流的语言就是 JavaScript。与传统的面对象编程的比较如下。
在基于类的编程当中,对象总共有两种类型。类定义了对象的基本布局和函数特性,而接口是“可以使用的”对象,它基于特定类的样式。在此模型中,类表现为行为和结构的集合,对所有接口来说这些类的行为和结构都是相同的。因而,区分规则首先是基于行为和结构,而后才是状态。
原型编程的主张者经常争论说,基于类的语言提倡使用一个关注分类和类之间关系的开发模型。与此相对,原型编程看起来提倡程序员关注一系列对象实例的行为,而之后才关心如何将这些对象划分到最近的使用方式相似的原型对象,而不是分成类。
因为如此,很多基于原型的系统提倡运行时原型的修改,而只有极少数基于类的面向对象系统(比如第一个动态面向对象的系统 Smalltalk)允许类在程序运行时被修改。
在基于类的语言中,一个新的实例通过类构造器和构造器可选的参数来构造,结果实例由类选定的行为和布局创建模型。
在基于原型的系统中构造对象有两种方法,通过复制已有的对象或者通过扩展空对象创建。很多基于原型的系统提倡运行时原型的修改,而基于类的面向对象系统只有动态语言允许类在运行时被修改(Common Lisp、Dylan、Objective-C、Perl、Python、Ruby 和 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
,这两个东西很容易混淆。这里说明一下:
__proto__
主要是安放在一个实际的对象中,用它来产生一个链接,一个原型链连,用于寻找方法名或属性,等等。prototype
是用new
来创建一个对象时构造__proto__
用的。它是构造函数的一个属性。
在 JavaScript 中,对象有两种表现形式, 一种是Object
(ES5 关于 Object 的文档),一种是Function
(ES5 关于 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。这样一来,对于实例b
或c
来说,就能访问到这个继承的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()
;然后将这个空的函数对象
new
出另一个对象,存在p
中;这时再改变
Person.prototype
,让其有一个name
的属性和一个sayHello()
的方法;我们发现,另外那个
p
的对象也跟着一起改变了。
注意一下,
当创建
function Person(){}
时,Person.__proto__
指向Function.prototype
;当创建
var p = new Person()
时,p.__proto__
指向Person.prototype
;当修改了
Person.prototype
的内容后,p.__proto__
的内容也就被改变了。
好了,我们再来看一下“原型编程”中面向对象的编程玩法。
首先,我们定义一个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);
};
}
上面这个对象中,包含了:
属性:
fullName
和email
;方法:
speak()
和introduction()
。
其实,所谓的方法也是属性。
然后,我们可以定义一个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!");
};
};
在上面的代码中:
使用了
Person.call(this, fullName, email)
,call()
或apply()
都是为了动态改变this
所指向的对象的内容而出现的。这里的this
就是Student
。上面的例子中,我们重载了
introduction()
方法,并新增加了一个takeExams()
的方法。
虽然,我们这样定义了Student
,但是它还没有和Person
发生继承关系。为了要让它们发生关系,我们就需要修改Student
的原型。
我们可以简单粗暴地做赋值:Student.__proto__ = Person.prototype
,但是,这太粗暴了。
我们还是使用比较规范的方式:
先用
Object.create()
来将Person.prototype
和Student.prototype
关联上。然后,修改一下构造函数
Student.prototype.constructor = Student;
。
// 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",
"[email protected]",
"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,给它增加甚至删除属性和方法。但是其带来了执行的不确定性,也有安全性的问题,而代码还变得不可预测,这有点黑科技的味道了。因为这些不像静态类型系统,没有一个不可变的契约对代码的确定性有保证,所以,需要使用者来自己保证。