原型
在使用JavaScript的面向对象编程中,原型对象是个核心概念。在JavaScript中对象是作为现有示例对象(即原型)的副本而创建的,该名称就来自于这一概念。此原型对象的任何属性和方法都将显示为从原型的构造函数创建的对象的属性和方法。可以说,这些对象从其原型继承了属性和方法。当您创建如下所示的新Dog对象时:
var buddy = new Dog('Buddy');
buddy所引用的对象将从它的原型继承属性和方法,尽管仅从这一行可能无法明确判断原型来自哪里,对象buddy的原型来自构造函数(在这里是函数Dog)的属性。
在JavaScript中,每个函数都有名为"prototype"的属性,用于引用原型对象。此原型对象又有名为"constructor"的属性,它反过来引用函数本身。这是一种循环引用,图3更好地说明了这种循环关系。
图 3 每个函数的原型都有一个 Constructor 属性
现在,通过"new"运算符用函数(上面示例中为Dog)创建对象时,所创建的对象将继承Dog.prototype属性。在图3中,可以看到Dog.prototype对象有一个回指Dog函数的构造函数属性。这样,每个Dog对象(从Dog.prototype继承而来)都有一个回指Dog函数的构造函数属性。图4中的代码证实了这一点。图5显示了构造函数、原型对象以及它们创建的对象之间的这一关系。
var spot = new Dog(“Spot”); // Dog.prototype is the prototype of spot alert(Dog.prototype.isPrototypeOf(spot)); // 说明spot对象是从原型继承来的 // spot inherits the constructor property // from Dog.prototype alert(spot.constructor == Dog.prototype.constructor); /* * 上例说明: * 说明构造函数都有一个名为"prototype"的属性,该属性指向原型, * 而原型又有一个指向构造函数的属性"constructor",很显然这是 * 一个圈(循环引用)。 */ alert(spot.constructor == Dog); /* * 上例说明: * 因为每个对象都继承自原型,所以每个对象都从原型中继承了constructor * 属性,而这个属性指向构造函数。 */ // But constructor property doesn’t belong // to spot. The line below displays “false” alert(spot.hasOwnProperty(“constructor”)); // The constructor property belongs to Dog.prototype // The line below displays “true” alert(Dog.prototype.hasOwnProperty(“constructor”)); / * * 上面两个例子第一个alert弹出的提示是false,第二个是true, * 这再次说明了,"constructor"属性不是对象本身的属性, * 而是来自于对象的原型。 */
图 5 实例继承其原型
某些读者可能已经注意到图4中对hasOwnProperty和isPrototypeOf方法的调用。这些方法是从哪里来的呢〉它们不是来自Dog.prototype。实际上,在Dog.prototype和Dog实例中还可以调用其他方法,比如:toString()、toLocaleString和valueOf,但它们都不来自Dog.prototype。您会发现,就像.NET Framework中的System.Object充当所有类的最终基类一样,JavaScript中的Object.prototype是所有原型的最终基础原型。(Object.prototype的原型是null)
在此示例中,请记住Dog.prototype是对象。它是通过调用Object构造函数创建的(尽管它不可见):
Dog.prototype = new Object();
因此,正如Dog实例继承Dog.prototype一样,Dog.prototype继承Object.prototype。这使得所有Dog实例也继承了Object.prototype的方法和属性。所以每个Dog实例也就继承了我们上面说的toString、toLocaleString、hasOwnProperty方法了。
每个JavaScript对象都继承一个原型链,而所有原型都终止于Object.prototype。注意,迄今为止您看到的这种继承是活动对象之间的继承。它不同于继承的常见概念,后者是指在声明类时类之间的发生的继承,因此,JavaScript继承动态性更强。它使用简单算法实现这一点,如下所示:当您尝试访问对象的属性/方法时,JavaScript将检查该属性/方法是否是在该对象中定义的。如果不是,则检查对象的原型,如果还不是,则检查该对象的原型的原型,如此继续,一直检查到Object.prototype。图6说明了此解析过程。
图 6 在原型链中解析 toString() 方法
JavaScript动态地解析属性访问和方法调用的方式产生了一些特殊效果:
图7说明了这些效果。图7还显示了如何解决前面遇到的不需要的方法实例的问题。通过将方法放在原型内部,可以使对象共享方法,而不必使每个对象都有单独的函数对象实例。在此示例中,rover和spot共享getBreed方法,直至在spot中以任何方式改写toString方法。此后,spot有了它自己版本getBreed方法,但rover对象和用新GreatDane创建的后续对象仍共享在GreatDane.prototype对象中定义的那个getBreed方法实例。
Figure 7 继承原型
function GreatDane() { } var rover = new GreatDane(); var spot = new GreatDane(); GreatDane.prototype.getBreed = function() { return 'Great Dane'; }; // Works, even though at this point // rover and spot are already created. alert(rover.getBreed()); // this hides getBreed() in GreatDane.prototype spot.getBreed = function() { return 'Little Great Dane'; }; alert(spot.getBreed()); // but of course, the change to getBreed // doesn’t propagate back to GreatDane.prototype // and other objects inheriting from it, // it only happens in the spot object alert(rover.getBreed());