原型链
正如我们之前所了解的,JavaScript中的每个函数都有一个名为prototype的对象属性。该函数被new操作符调用时会创建出一个对象,并且该对象中会有一个指向其原型对象的秘密链接(在某些环境中,该链接名为__proto__),我们就可以在新建的对象中调用相关原型对象的方法和属性。
而原型对象自身也具有对象固有的普遍特征,因此本身也包含了指向其原型的链接。由此就形成了一条链,我们称之为原型链。
如图所示,在对象A的一系列属性中,有一个叫做__proto__的隐藏属性,它指向了另一个对象B(对象A的构造函数的原型)。而B的__proto__属性又指向了对象C,以此类推,直至链条末端的Object对象,该对象时JavaScript中的最高级父对象(没有给原型进行定义的函数,默认值Object)。
正因为有了这些技术,我们才可以在某个属性不在对象A中而在对象B中时,依然将它当做A的属性来访问(基于对象的继承)。同样的,如果对象B中也没有该属性,还可以继续到对象C中去寻找。这就是继承的作用,它能使每个对象都能访问其继承链上的任何一个属性。
原型链示例
首先我们定义三个构造器函数
function Shape(){ this.name = 'shape'; this.toString = function(){ return this.name; }; } function TwoDShape(){ this.name = '2D shape'; } function Triangle(side,height){ this.name = 'Triangle'; this.side = side; this.height = height; this.getArea = function(){return this.side * this.height/2;}; }
接下来,就是我们施展继承魔法的代码了:
TwoDShape.prototype = new Shape(); Triangle.prototype = new TwoDShape();
我们将对象new Sharpe()直接创建在TwoDShape构造函数的prototype属性中,下面我们来测试一下目前为止所实现的内容,先创建一个Triangle对象,然后调用它的getArea()方法:
尽管对象my中并没有属于自己的toString()方法,但我们依然可以调用它所继承的方法。并且该方法toString()显然是与my对象紧密绑定在一起的。
- 接下来,我们关注一下JavaScript引擎在my.toString()被调用时究竟做了哪些事
- 首先,它会遍历my对象中的所有属性,但没有找到一个叫做toString()的方法。
- 接着再去查看my.__proto__所指向的对象,该对象是my构造函数的原型,就是new TwoDShape()所创建的实体。
- 显然,JavaScript引擎在遍历TwoDShape实体的过程中依然不会找到toString()方法。然后,它又会继续检查该实体的__proto__属性。这时候,该__proto__属性所指向的实体是由new Shape()所创建的。
- 终于,在new Shape()所创建的实体中找到了toString()方法。
- 最后,该方法就会在my对象中被调用,并且其this也指向了my。
通过instanceof操作符,我们可以验证my对象同时是上述三个构造器的实例:
同样,当我们以my参数调用这些构造器原型的isPropertypeOf()方法时,结果也是如此:
将共享属性迁移到原型中去
当我们用某一个构造器创建对象时,其属性就被添加到this中去。例如,在上面的实例中,Shape()构造器是这样定义的:
function Shape(){ this.name = 'shape'; }
这种实现意味着每当我们用new Shape()新建对象时,每个实体都会有一个全新的name属性,并在内存中拥有自己独立的存储空间。而事实上,我们也可以选择将name属性添加到所有实体所共享的原型对象中去:
function Shape(){} Shape.prototype.name = 'shape';
这样一来,每当我们再用new Shape()新建对象时,新对象中就不再含有属于自己的name属性了,而是被添加进了该对象的原型中。虽然这样做通常会更有效率,但这也只是针对对象实体中的不可变属性而言的,另外,这种方式也同样使用于对象中的共享性方法。
修改后的代码:
function Shape(){} Shape.prototype.name='Shape'; Shape.prototype.toString=function(){return this.name}; function TwoDShape(){} TwoDShape.prototype=new Shape(); TwoDShape.prototype.constructor=TwoDShape; TwoDShape.prototype.name='2D Shape'; function Triangle(side,height){ this.side = side; this.height = height; } Triangle.prototype=new TwoDShape(); Triangle.prototype.constructor=Triangle; Triangle.prototype.name='Triangle'; Triangle.prototype.getArea=function(){return this.side*this.height/2;};
在我们完成相关的继承关系设定后,对这些对象的constructor属性进行相应的重置是一个非常好的习惯。另外,我们通常会在对原型对象进行扩展前,先完成相关对象的关系构建,因为后面的新内容(name='')有时会抹掉我们继承来的东西。