JavaScript 原型继承?

16

我一直在学习 JavaScript 的继承,以便更好地理解它,但我发现了一些令我困惑的事情。

我知道当你使用 new 关键字调用一个“构造函数”时,你会得到一个新对象,并引用该函数的原型。

我也知道为了进行原型继承,你必须将构造函数的原型替换为所需“超类”的实例。

因此,我创建了这个简单的示例来尝试这些概念:

function Animal(){}
function Dog(){}

Animal.prototype.run = function(){alert("running...")};

Dog.prototype = new Animal(); 
Dog.prototype.bark = function(){alert("arf!")};

var fido = new Dog();
fido.bark() //ok
fido.run() //ok

console.log(Dog.prototype) // its an 'Object' 
console.log(fido.prototype) // UNDEFINED
console.log(fido.constructor.prototype == Dog.prototype) //this is true

function KillerDog(){};
KillerDog.prototype.deathBite = function(){alert("AAARFFF! *bite*")}

fido.prototype = new KillerDog();

console.log(fido.prototype) // no longer UNDEFINED
fido.deathBite(); // but this doesn't work!

(这是在Firebug的控制台中完成的)

1)为什么如果所有新对象都包含对创建函数原型的引用,fido.prototype未定义?

2)继承链[obj] -> [constructor] -> [prototype]是否代替[obj] -> [prototype]?

3)我们对象(fido)的“prototype”属性是否被检查过?如果是...为什么最后一部分“deathBite”未定义?

5个回答

12

1) 如果所有新对象都包含对创建函数原型的引用,为什么fido.prototype是未定义的?

所有新对象确实持有一个对它们构造函数在构造时存在的原型的引用。然而用于存储此引用的属性名称不是像构造函数本身上的prototype那样。一些JavaScript实现允许通过某些属性名(例如__proto__)访问此“隐藏”属性,而其他实现则不允许(例如Microsoft的实现)。

2) 继承链[obj] -> [constructor] -> [prototype]而不是[obj] -> [prototype]吗?

不是。看一下这个例子:

function Base() {}
Base.prototype.doThis = function() { alert("First"); }

function Base2() {}
Base2.prototype.doThis = function() { alert("Second"); }

function Derived() {}
Derived.prototype = new Base()

var x = new Derived()

Derived.prototype = new Base2()

x.doThis();

这会触发“First”,而不是“Second”。如果继承链通过构造函数传递,我们将看到“Second”。当对象被构造时,函数原型属性中保存的当前引用会被传递给对象隐藏引用其原型的属性。

3)我们的对象(fido)的“prototype”属性是否被检查过? 如果是...为什么最后部分的“deathBite”未定义?

给除了函数之外的对象分配一个名为prototype的属性没有特殊含义,正如前面所述,对象不会通过此属性名称维护对其原型的引用。


1
不错!请注意,如果您在Derived.prototype = new Base2()之后声明var x = new Derived(),则会看到“Second”。x只是指向原始Derived.prototype的指针,但这并不意味着我们实际上没有将其重定向到Base2()。指针只是被更改了。这与Anthony所说的完全不矛盾,只是澄清了最后一点。请参阅我的示例:http://github.com/roblevintennis/Testing-and-Debugging-JavaScript/blob/master/code/objects/lib/js_inheritance.js - Rob
哦,我想你的意思是:proto 而不是 __prototype__。 - Rob
Markdown,呃!下划线下划线proto下划线下划线 - Rob
问者所忽略的一点是,原型属性只存在于构造函数上,而不是对象实例上。因此,如果您有一个实例并且想知道它的原型,必须调用 instance.constructor.prototype。但是,正如注意到的那样,设置它不会改变实例的原型,只会改变构造函数的原型。 - Ruan Mendes

7

一旦使用new实例化对象,就无法更改其原型。

在上面的示例中,诸如

fido.prototype = new KillerDog();

这段代码在对象 fido 上简单地创建了一个名为 prototype 的新属性,并将该属性设置为一个新的 KillerDog 对象。这与其他操作没有什么不同。

fido.foo = new KillerDog();

根据您的代码...

// Doesn't work because objects can't be changed via their constructors
fido.deathBite();

// Does work, because objects can be changed dynamically, 
// and Javascript won't complain when you use prototype 
//as an object attribute name
fido.prototype.deathBite();

特殊的prototype行为仅适用于javascript中的构造函数,其中构造函数是将使用new调用的functions。

你可以在Mozilla(Firefox等)中做到。但是,通常这是不切实际的。 - thomasrutter

4

针对您的问题进行编号回答:

  1. 对象的原型属性并不叫做prototype。标准使用[[prototype]]来指定它。Firefox将此属性公开为__proto__。
  2. 继承链是[obj][原型对象]。您最初的假设([obj][构造函数][原型])是错误的,您可以通过修改constructor和/或constructor.prototype并检查可以在您的[obj]上调用哪些方法来轻松证明这一点 - 您会发现这些修改不会改变任何内容。
  3. 对象上的prototype属性未被检查和未被使用。您可以将其设置为任何您喜欢的值。JavaScript仅在对象构建期间在函数对象上使用它。

为了演示第3点,以下是来自Dojo的代码:

dojo.delegate = dojo._delegate = (function(){
  // boodman/crockford delegation w/ cornford optimization
  function TMP(){}
  return function(obj, props){
    TMP.prototype = obj;
    var tmp = new TMP();
    if(props){
      dojo._mixin(tmp, props);
    }
    return tmp; // Object
  }
})();

如您所见,它利用了只在一个地方使用prototype的事实,通过重复使用相同的函数TMP来处理具有不同原型的所有委托对象。实际上,在使用new调用函数之前直接分配prototype,并且在此之后更改它不会影响任何已创建的对象。
您可以在我的回答中找到创建对象的顺序:JavaScript中[[Prototype]]和prototype之间的关系

2

我知道这个问题已经有了答案,但是有更好的继承方式。仅仅为了继承而调用构造函数并不是一个理想的做法。其中一个不希望出现的影响是。

function Base() {this.a = "A"}
function Child() {this.b = "B"};

Child.prototype = new Base();

现在您已经将不打算添加的属性"a"添加到Child的原型中。

这是正确的方法(我并没有发明这个,Ext-JS和其他库使用此方法)

// This is used to avoid calling a base class's constructor just to setup inheritance.
function SurrogateCtor() {}

/**
 * Sets a contructor to inherit from another constructor
 */
function extend(BaseCtor, DerivedCtor) {
  // Copy the prototype to the surrogate constructor
  SurrogateCtor.prototype = BaseCtor.prototype;
  // this sets up the inheritance chain
  DerivedCtor.prototype = new SurrogateCtor();
  // Fix the constructor property, otherwise it would point to the BaseCtor
  DerivedCtor.prototype.constructor = DerivedCtor;
  // Might as well add a property to the constructor to 
  // allow for simpler calling of base class's method
  DerivedCtor.superclass = BaseCtor;
}

function Base() {
  this.a = "A";
}

Base.prototype.getA = function() {return this.a}

function Derived() {
  Derived.superclass.call(this);  // No need to reference the base class by name
  this.b = "B";
}

extend(Base, Derived);
// Have to set methods on the prototype after the call to extend
// otherwise the prototype is overridden;
Derived.prototype.getB = function(){return this.b};
var obj = new Derived();

更简单的方法是在扩展时添加第三个参数,指定派生类的方法,这样您就不必调用extend并将方法添加到原型中。

extend(BaseCtor, DerivedCtor, {
  getB: function() {return this.b}
});

除此之外,您还可以为语法糖做许多其他事情。

在博客中写到:http://js-bits.blogspot.com/2010/08/javascript-inheritance-done-right.html


2

值得注意的是,在ECMAScript 5(即JavaScript语言的最新版本)中,您可以通过Object.getPrototypeOf访问实例的内部[[Prototype]]属性:

Object.getPrototypeOf(fido) === Dog.prototype

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接