前言
最近在学习设计模式,正好看到了原型模式,解开了我以前的一些疑惑。也趁着复习的积极性,来回顾和加深一下JavaScript基于原型模式的面向对象机制。
原型模式
首先得弄清楚一些概念,JavaScript是没有类的概念的,对是没有的。JavaScript是一门基于原型模式的语言,什么是原型模式?先来看一个例子:
var Person = function(name) {
this.name = name;
}
let tom = new Person('tom');
以上例子中,如果熟悉面向对象的同学很快就会发现,这是一个类的经典继承例子,但准确来说,在JavaScript,Person不是一个类,是一个函数构造器,也称为构造函数。 而且是通过原型链实现继承的。
对象的定义
得先看一下ECMAScript中关于对象的定义:
An object is a collection of properties and has a single prototype object. The prototype may be either an object or the null value.
Object是一个属性的集合,并且都拥有一个单独的原型对象[prototype object]. 这个原型对象[prototype object]可以是一个object或者null值。
意思就是每一个对象拥有一个原型对象,我们将它称为对象的原型,一个对象的原型是一个内部的[[prototype]]属性的引用。而浏览器厂商对这个属性的实现通常是__proto__
,通过__proto__
可以找到对象的原型对象。
我们可以还通过手动设置__proto__
来调整对象的原型。
let foo = {
x: 10,
y: 20
};

可以看到foo有两个显式属性x和y,还有一个__proto__
,指向它的原型对象。
对象是怎么创建的
在基于原型模式的语言系统中,有以下的特点,JavaScript也是如此:
要得到一个对象,不是通过实例类,而是找到一个对象作为一个原形并克隆它
实际上,JavaScript中的根对象是Object.prototype对象,所有的对象都是从Object.prototype对象克隆而来的,Object.prototype就是它们的原型。
let obj1 = new Object();
let obj2 = {}
// 利用ES5的Object.getPrototypeOf获得对象的原型
console.log(Object.getPrototypeOf(obj1) === Object.prototype) //true
console.log(Object.getPrototypeOf(obj2) === Object.prototype) //true
当我们调用new或者字面量创建一个对象的时候,内部引擎都会从Object.prototype上克隆一个对象出来,同时做了一些额外的处理,我们不需要关心这些细节,但可以通过一些例子验证这个结论。
现在看回文章开头的例子,调用new Person创建了一个对象tom,熟悉new的过程的同学会发现,在new的内部,首先是创建了一个新的对象,这个对象就是从Object.prototype上克隆而来的, 然后再改变新的对象的原型对象,改为Person.prototype,如果成功返回这个新对象,这个新对象就是tom。
所以tom的原型对象是Person.prototype
console.log(Object.getPrototypeOf(tom) === Person.prototype) //true
那么为什么要引入原型对象这个概念,这里又要引出一个概念:原型链
原型链的定义
ECMAScript中关于原型链(Prototype chain)的定义:
A prototype chain is a finite chain of objects which is used to implemented inheritance and shared properties. 原型链是一个由对象组成的,用于实现继承和共享属性的有限链
通俗来讲,原型对象也是普通的对象,并且也有可能有自己的原型,如果一个原型对象的原型不为null的话,我们就称之为原型链(prototype chain)。
在基于类[class-based]的系统中,重用相同的方法或者属性叫做类的继承,而原型链的设计也是为了重用代码,通过原型链实现的继承称为基于委托的继承 (delegation based inheritance),也叫做原型继承。
为什么说是基于委托的继承呢?看下面的例子
let a = {
x: 10,
print: function() {
return this.x + this.y;
}
};
let b = {
y: 20,
__proto__: a
}
b.print() // 30
以上的例子中,调用b.print()
会执行以下过程:
- 首先尝试遍历对象b中的所有属性,但没有找到print方法
- 将查找print的请求委托给对象b的原型,在上面的例子中手动设置了b的
__proto__
为a,所以b的原型是a对象 - 在a对象中遍历查找print方法,如果找到了第一个print方法,返回它
- 如果在a中也没有找到print方法,会将请求委托给a的原型对象,重复这个过程直到遍历完整个原型链(null),如果没有找到,返回undefined
值得注意的是,当b调用print函数的时候,this是指向b的,所以y是20,而b中没有x,this.x是从原型链上查找的,也就是a的10,所以才会打印出30。
构造函数
构造函数(constructor) 做了一件有用的事情,自动为创建的新对象设置了原型对象(prototype object) 。原型对象存放于 ConstructorFunction.prototype 属性中。
let obj = new Object();
obj.__proto__ === Objcet.prototype; // true;
别忘了,Object也是一个构造函数,typeof Object
输出的应该是"function"
// 构造函数
function Foo(y) {
// 构造函数将会以特定模式创建对象:被创建的对象都会有"y"属性
this.y = y;
}
// "Foo.prototype"存放了新建对象的原型引用
// 所以我们可以将之用于定义继承和共享属性或方法
// 所以,和上例一样,我们有了如下代码:
// 继承属性"x"
Foo.prototype.x = 10;
// 继承方法"calculate"
Foo.prototype.calculate = function (z) {
return this.x + this.y + z;
};
// 使用foo模式创建 "b" and "c"
var b = new Foo(20);
var c = new Foo(30);
// 调用继承的方法
b.calculate(30); // 60
c.calculate(40); // 80
// 让我们看看是否使用了预期的属性
console.log(
b.__proto__ === Foo.prototype, // true
c.__proto__ === Foo.prototype, // true
// "Foo.prototype"自动创建了一个特殊的属性"constructor"
// 指向a的构造函数本身
// 实例"b"和"c"可以通过授权找到它并用以检测自己的构造函数
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
)
构造函数本身也是一个对象,所以构造函数也有自己的原型,也就是__proto__
属性,一般指向Function.prototype,而Function.prototype的原形是Object.prototype.
以上例子可以用这个图描述。

再来分析一个例子,通常我们想要实现一个“类“继承另一个“类“的时候,在JS中通常这么写:
let A = function() {};
A.prototype.name = 'A'
let a = new A();
let B = function() {};
B.prototype = a;
let b = new B();
console.log(b.name); // A;
new A
后,返回一个对象a,a.__proto__ === A.prototype // true
,将B.prototype
设置为a,所以B.prototype.__proto__ === A.prototype // true
,
new B
后,返回一个对象b,b.__proto__ === B.prototype // true
,则b.__proto__.__proto__ === A.prototype // true
,实现了原型链的连接
图我就不画了,懒。
多提一嘴,Object.create()
来实现继承更能体现原型模式的精髓,但好像效率没有构造函数高,我们还可以通过Object.create(null)
来创建一个没有原型的对象。

参考
《javascript 设计模式与开发实践》