12

我是 Javascript 编程的新手,我正在从面向对象编程的角度来处理我的第一个应用程序(实际上是一个游戏)(我知道 js 并不是真正面向对象的,但是对于这个特殊问题,我更容易开始喜欢这)。

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

最小的例子是:

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');

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

也许我不了解原型如何在 javascript 中工作。我知道该功能在所有对象之间共享,但我想执行将是单独的。有人可以解释为什么会发生这种情况以及如何解决它吗?我不知道我是否做错了继承,或者原型定义,或者什么。

提前致谢。

4

3 回答 3

23

欢迎来到原型链!

让我们看看它在您的示例中是什么样子的。

问题

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

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

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

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

所以每个实例ThingA都会继承自该Thing实例。现在您将创建ThingA1并为它们的每个原型ThingA2分配一个新ThingA实例,然后创建 and 的实例ThingA1ThingA2ThingAand Thing,但此处未显示)。

现在的关系是这样的(__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__  |-----------+
+-------------+                        

正因为如此,ThingA, ThingA1or的每个实例都ThingA2 指向同一个数组实例

不是你想要的!


解决方案

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

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

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

// ...

这会在这些函数内部调用ThingandThingA并将其设置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属性,但它们都被实例属性所遮蔽。


更好的继承

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

ThingA.prototype = new Thing();

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

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

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

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

于 2013-02-23T13:55:56.400 回答
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
于 2014-01-22T16:19:11.650 回答
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 数组实例。

于 2013-02-23T14:05:19.700 回答