Javascript“OOP”和具有多级继承的原型

时间:2013-02-23 13:17:58

标签: javascript inheritance prototype

我是Javascript编程的新手,我正在从面向对象的编程角度接近我的第一个应用程序(确实是游戏)(我知道js不是真正的面向对象,但对于这个特殊的问题,它对我来说更容易像这样开始)。

我有一个“类”的层次结构,其中最顶层(“Thing”类)定义了相关事物的列表(游戏中的附加项)。它由ThingA类继承,由ThingA1和ThingA2类继承。

最小的例子是:

function Thing() 
{
  this.relatedThings   = [];
}
Thing.prototype.relateThing = function(what)
{
  this.relatedThings.push(what);
}

ThingA.prototype = new Thing();
ThingA.prototype.constructor = ThingA;
function ThingA()
{
}

ThingA1.prototype = new ThingA();
ThingA1.prototype.constructor = ThingA1;
function ThingA1()
{

}

ThingA2.prototype = new ThingA();
ThingA2.prototype.constructor = ThingA2;
function ThingA2()
{    
}

var thingList = [];

thingList.push(new ThingA());
thingList.push(new ThingA1());
thingList.push(new ThingA2());
thingList.push(new ThingA2());
thingList.push(new Thing());

thingList[1].relateThing('hello');

在代码结束时,当执行relatedThing时,每个ThingA,ThingA1和ThingA2将执行它(不是数组中的最后一个“Thing”对象)。我发现如果我在ThingA原型中定义了relatedThing函数,它将正常工作。由于游戏的设计方式,我宁愿不必这样做。

也许我不了解原型如何在javascript中运行。我知道函数在所有对象之间共享,但我想执行将是个体的。有人可以解释为什么会发生这种情况以及如何解决这个问题?我不知道我做的继承是错误的,还是原型定义,或者是什么。

提前致谢。

3 个答案:

答案 0 :(得分:23)

欢迎来到原型链!

让我们看看你的例子中的样子。

问题

当您致电new Thing()时,您正在创建一个新对象,其属性relatedThings引用一个数组。所以我们可以说我们有这个:

+--------------+
|Thing instance|
|              |
| relatedThings|----> Array
+--------------+     

然后,您将此实例分配给ThingA.prototype

+--------------+
|    ThingA    |      +--------------+
|              |      |Thing instance|
|   prototype  |----> |              |
+--------------+      | relatedThings|----> Array
                      +--------------+

因此ThingA的每个实例都将从Thing实例继承。现在,您将创建ThingA1ThingA2并为每个原型分配一个新的ThingA实例,然后创建ThingA1ThingA2的实例(和ThingAThing,但此处未显示)。

现在关系就是这个(__proto__内部属性,将对象与其原型连接起来):

                               +-------------+
                               |   ThingA    |
                               |             |    
+-------------+                |  prototype  |----+
|   ThingA1   |                +-------------+    |
|             |                                   |
|  prototype  |---> +--------------+              |
+-------------+     |    ThingA    |              |
                    | instance (1) |              |
                    |              |              |
+-------------+     |  __proto__   |--------------+ 
|   ThingA1   |     +--------------+              |
|   instance  |           ^                       |
|             |           |                       v
|  __proto__  |-----------+                 +--------------+
+-------------+                             |Thing instance|
                                            |              |
                                            | relatedThings|---> Array
+-------------+     +--------------+        +--------------+ 
|   ThingA2   |     |   ThingA     |              ^
|             |     | instance (2) |              |
|  prototype  |---> |              |              |
+-------------+     |  __proto__   |--------------+
                    +--------------+
+-------------+           ^
|   ThingA2   |           |  
|   instance  |           |
|             |           |
|  __proto__  |-----------+
+-------------+                        

正因为如此,ThingAThingA1ThingA2 的每个实例都引用同一个数组实例

这是你想要的东西!


解决方案

要解决此问题,任何“子类”的每个实例都应具有自己的 relatedThings属性。您可以通过在每个子构造函数中调用父构造函数来实现此目的,类似于在其他语言中调用super()

function ThingA() {
    Thing.call(this);
}

function ThingA1() {
    ThingA.call(this);
}

// ...

这会调用ThingThingA,并将这些函数中的this设置为您传递给.call的第一个参数。详细了解.call [MDN]this [MDN]

仅此一项就会将上面的图片更改为:

                               +-------------+
                               |   ThingA    |
                               |             |    
+-------------+                |  prototype  |----+
|   ThingA1   |                +-------------+    |
|             |                                   |
|  prototype  |---> +--------------+              |
+-------------+     |    ThingA    |              |
                    | instance (1) |              |
                    |              |              |
                    | relatedThings|---> Array    |
+-------------+     |  __proto__   |--------------+ 
|   ThingA1   |     +--------------+              |
|   instance  |           ^                       |
|             |           |                       |
|relatedThings|---> Array |                       v
|  __proto__  |-----------+                 +--------------+
+-------------+                             |Thing instance|
                                            |              |
                                            | relatedThings|---> Array
+-------------+     +--------------+        +--------------+ 
|   ThingA2   |     |   ThingA     |              ^
|             |     | instance (2) |              |
|  prototype  |---> |              |              |
+-------------+     | relatedThings|---> Array    |
                    |  __proto__   |--------------+
                    +--------------+
+-------------+           ^
|   ThingA2   |           |  
|   instance  |           |
|             |           |
|relatedThings|---> Array | 
|  __proto__  |-----------+
+-------------+

如您所见,每个实例都有自己的relatedThings属性,该属性引用不同的数组实例。原型链中仍有relatedThings个属性,但实例属性都是 shadowed


更好的继承

另外,不要将原型设置为:

ThingA.prototype = new Thing();

您实际上不想在此处创建新的Thing实例。如果Thing预期参数会发生什么?你会通过哪一个?如果调用Thing构造函数有副作用怎么办?

实际需要的是将Thing.prototype连接到原型链中。您可以使用Object.create [MDN]执行此操作:

ThingA.prototype = Object.create(Thing.prototype);

执行构造函数(Thing)时发生的任何事情都会在我们实际创建新的ThingA实例时发生(通过调用Thing.call(this),如上所示)。

答案 1 :(得分:1)

如果您不喜欢原型设计在JavaScript中的工作方式,以实现简单的继承和OOP方式,我建议您看一下:https://github.com/haroldiedema/joii

它基本上允许您执行以下操作(以及更多):

// First (bottom level)
var Person = new Class(function() {
    this.name = "Unknown Person";
});

// Employee, extend on Person & apply the Role property.
var Employee = new Class({ extends: Person }, function() {
    this.name = 'Unknown Employee';
    this.role = 'Employee';
});

// 3rd level, extend on Employee. Modify existing properties.
var Manager = new Class({ extends: Employee }, function() {

    // Overwrite the value of 'role'.
    this.role = 'Manager';

    // Class constructor to apply the given 'name' value.
    this.__construct = function(name) {
        this.name = name;
    }
});

// And to use the final result:
var myManager = new Manager("John Smith");
console.log( myManager.name ); // John Smith
console.log( myManager.role ); // Manager

答案 2 :(得分:0)

在OOP中,当您定义子类的构造函数时,您也(隐式或显式地)从超类型中选择构造函数。当构造子对象时,两个构造函数都被执行,首先是从超类中执行,所有其他的都在层次结构中。

在javascript中必须明确调用,不要自动生成!

function Thing() {
  this.relatedThings = [];
}
Thing.prototype.relateThing = function(what){
  this.relatedThings.push(what);
}

function ThingA(){
  Thing.call(this);
}
ThingA.prototype = new Thing();

function ThingA1(){
  ThingA.call(this);
}
ThingA1.prototype = new ThingA();

function ThingA2(){
  ThingA.call(this);

}
ThingA2.prototype = new ThingA();

如果你不这样做,ThingA,ThingA1和ThingA2的所有实例都是在Thing构造函数中构造的相同的relatedThings数组,并且在一行中为所有实例调用一次:

ThingA.prototype = new Thing();

在全球范围内,事实上,在您的代码中,您只有2次调用Thing构造函数,这只会产生2个relatedThings数组的实例。