原型链机制是javascript中实现继承的关键。对于prototype对象(原型对象)的理解至关重要。下文根据自己在开发中以及看书过程中对于prototype对象的理解整理了一些小tip。

何为原型对象

原型对象是什么?在《JavaScript高级编程》一书中是这样说的:

无论什么时候,只要创建了一个新的函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指 向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。

综上所述,所有的函数对象都会有一个prototype属性,该属性指向该函数的原型对象。也就是说原型对象的概念是基于函数对象来说的。

深入理解原型对象

javascript中一切皆对象。所有对象原型链的终点都指向Object.prototype。javascript中存在一类这样的函数,称之为构造函数。其可以通过new运算符生产对象,通过new运算符生产的对象称之为实例对象。如何理解实例对象与构造函数对象以及构造函数对象的原型对象三者之间的联系是理解javascript中原型链机制以及继承机制的关键。上文中已经提及,在函数对象中有prototype属性指向该函数的原型对象。构造函数也不例外。其原型对象往往定义了一些该构造函数中需要共享的属性。何为共享?即通过new运算符产生的实例对象对于其构造函数对象的共享,也就是平常我们所说的子类对于父类属性或者方法的共享。javascript中通常将需要一些实例的私有属性定义在构造函数中,将一些实例需要共享的属性定义在构造函数的原型对象中。比如:

1
2
3
4
5
6
7
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.sayName = function(){
console.log(this.name);
}

在上述实例中的构造函数即为该模式,我们可以通过new的方式将Person实例化

1
2
3
4
var person1 = new Person('monster1',23);
person1.sayName();//23
var person2 = new Person('monster2',24);
peroson2.sayName();//24

通过new的方式实例化了两个Person对象,person1和person2。两个对象都有自己的私有属性name和age,可以通过打印person1对象来验证,两者共享sayName方法,可以通过调用该方法打印各自对象的name属性值。

原型模式的原理

通过new的方式产生实例对象的过程,原理上是通过实例对象的_proto_属性建立与构造函数的原型对象之间的链接。实例对象的_proto_属性被部分浏览器实现了,但是不能访问到该属性。该属性保存的是一指针,指向构造函数的原型对象。在调用person1.sayName()方法时,javascript引擎首先查找person1的实例属性,person1的实例属性仅仅包括name和age。因此继续沿着原型链查找_proto_指向的原型对象。在原型对象中定义了该方法,因此调用sayName方法实际上是调用的构造函数的原型对象的原型方法。需要明确的是实例对象与构造函数对象之间并没有直接联系,实例对象仅仅指向的是构造函数的原型对象。 在原型对象中的constructor属性又指向了该构造函数。以上部分在《JavaScript高级编程》中给出了详细的图示。

原型对象的constructor属性

上文曾提及到,每个原型对象都会默认获得一个constructor属性,该属性指向其函数对象。此处需要注意的是给原型对象添加属性或者方法的时候以属性的方式添加不会断开与原来的原型对象之间的链接,仅仅是往原型对象中添加属性。但是通过对象字面量的方式给prototype赋值的时候会导致与原来的原型对象之间的链接断裂,这种方式相当于重写了原型对象,如果不显示赋值constructor属性的话会导致该原型对象没有constructor属性。此处理解的关键在于构造函数的prototype的属性是原型对象的一个指针。如果以对象字面量的方式指定prototype,相当于prototype重新指向了另外一个新的对象。

比如:

1
2
3
4
5
6
7
8
9
10
11
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.sayName = function(){
console.log(this.name);
}

var person1 = new Person('monster1935',24);

console.log(person1.constructor);//Person

如果是对象字面量的方式:

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype = {
sayName: function(){
console.log(this.name);
}
}
var person1 = new Person('monster1935',24);
console.log(person1.constructor);//Object
console.log(person1 instanceof Person);//true

第二种情况下重写了Person.prototype,因此其原型对象中默认不再有constructor属性。

关于constructor属性的作用一直比较疑惑,因为在平时的开发中很少使用到该属性。该属性并且也不是一个十分可靠的值,可以通过重写prototype任意指定。但是为了养成一个良好的习惯,即使在重写prototype对象后也应该显示的指定construtor的值,这样可以更好表明该实例对象的与父类之间的联系。虽然即使不指定constructor也不会影响其类型的判定。比如在第二种情况下使用instanceof还是能表明实例与父类之间的关系。因此个人对于constructor属性存在的意义仍没有很好的体会。不过可以使用person1.constructor.prototype显示的访问构造函数的原型对象。

小结

prototype是javascript中比较重要的一个概念。对于原型的理解可以很好的把握javascript的继承机制。
以上文字是个人对于prototype的一些体会。晚安。