我是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中运行。我知道函数在所有对象之间共享,但我想执行将是个体的。有人可以解释为什么会发生这种情况以及如何解决这个问题?我不知道我做的继承是错误的,还是原型定义,或者是什么。
提前致谢。
答案 0 :(得分:23)
欢迎来到原型链!
让我们看看你的例子中的样子。
当您致电new Thing()
时,您正在创建一个新对象,其属性relatedThings
引用一个数组。所以我们可以说我们有这个:
+--------------+
|Thing instance|
| |
| relatedThings|----> Array
+--------------+
然后,您将此实例分配给ThingA.prototype
:
+--------------+
| ThingA | +--------------+
| | |Thing instance|
| prototype |----> | |
+--------------+ | relatedThings|----> Array
+--------------+
因此ThingA
的每个实例都将从Thing
实例继承。现在,您将创建ThingA1
和ThingA2
并为每个原型分配一个新的ThingA
实例,然后创建ThingA1
和ThingA2
的实例(和ThingA
和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
,ThingA1
或ThingA2
的每个实例都引用同一个数组实例。
这是不你想要的东西!
要解决此问题,任何“子类”的每个实例都应具有自己的 relatedThings
属性。您可以通过在每个子构造函数中调用父构造函数来实现此目的,类似于在其他语言中调用super()
:
function ThingA() {
Thing.call(this);
}
function ThingA1() {
ThingA.call(this);
}
// ...
这会调用Thing
和ThingA
,并将这些函数中的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数组的实例。