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

Posted

技术标签:

【中文标题】Javascript“OOP”和具有多级继承的原型【英文标题】:Javascript "OOP" and prototypes with multiple-level inheritance 【发布时间】:2013-02-09 01:27:58 【问题描述】:

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

提前致谢。

【问题讨论】:

JavaScript 几乎都是基于对象的,为什么不应该是面向对象的呢? 据我所知,原型语言与面向对象的语言并不完全相同(没有类等),尽管概念可能相似。但我不是专家。 JavaScript 强烈面向对象这一事实并不意味着它的工作方式与其他 OO 语言相同。事实上,继承是不支持的。许多库实现了高级方法,这些方法确实是改进继承工作方式的解决方法。目前我发现的最好的之一是 John Resig 的,它基于 Prototype 和 base2 库,但进一步改进。 ejohn.org/blog/simple-javascript-inheritance 您的代码没有按预期工作吗?不明白你想要什么结果和你得到了什么 @Joel_Blum 执行后,所有对象在它们各自的relatedThings 数组中都有一个“hello”。我原以为只有 thingList[1] 会拥有它。 【参考方案1】:

欢迎来到原型链!

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

问题

当您调用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属性,但都被实例属性遮蔽了。


更好的继承

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

ThingA.prototype = new Thing();

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

实际上想要的是将Thing.prototype 连接到原型链中。你可以通过Object.create [MDN] 做到这一点:

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

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

【讨论】:

好吧。我会停止写我要写的东西。坚定的回应。 +1 @Felix :令人印象深刻的反应,我现在正在测试。只是另一个问题:我改变了我设置原型的方式,但应该以与我相同的方式指定构造函数吗?即:ThingA.prototype.constructor = ThingA; @neverbot:嗯,我希望它有帮助,也许它太混乱了:D 设置constructor 不是必需,因为没有内部方法使用它,但它是好的做法。 很好的解释。 +1 的努力。 漂亮的图表。对于最后一部分,在What is the reason (not) to use the 'new' keyword here? 有一些很好的答案。【参考方案2】:

如果您不喜欢 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

【讨论】:

【参考方案3】:

在 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 数组实例。

【讨论】:

@Felix Kling:感谢您的建议,我已更正。我也读了你的,恭喜:)

以上是关于Javascript“OOP”和具有多级继承的原型的主要内容,如果未能解决你的问题,请参考以下文章

玩转JavaScript OOP[4]——实现继承的12种套路

JavaScript对象及初识OOP

Javascript原型继承容易忽略的错误

javaScript设计模式之面向对象编程(object-oriented programming,OOP)--寄生组合式继承

JavaScript语言对继承的使用

第一百零九节,JavaScript面向对象与原型