JavaScript原型继承

16

这似乎是不一致的,但可能是因为我对JavaScript中的原型继承特性不熟悉。基本上,我有两个基类属性,“list”和“name”。我实例化了两个子类并给属性赋值。当我实例化第二个子类时,它从第一个子类实例获取了“list”值,但没有获取“name”值。到底发生了什么?当然,我希望任何后续的子类实例都不会从其他实例获取值,但如果发生了这种情况,它应该是一致的!

以下是代码片段:

    function A()
    {
        this.list = [];
        this.name = "A";
    }
    function B()
    {
    }
    B.prototype = new A();

    var obj1 = new B();
    obj1.list.push("From obj1");
    obj1.name = "obj1";

    var obj2 = new B();

    //output:
    //Obj2 list has 1 items and name is A
    //so, the name property of A did not get replicated, but the list did
    alert("Obj2 list has " + obj2.list.length + " items and name is "+ obj2.name);    
6个回答

13
使用原型的一个问题是,你可以从它们进行读取操作而不会改变它所指向的基础结构。但是,第一次赋值时,会在该实例上替换该属性所指向的内容。
在您的情况下,您并没有重新分配原型化的属性,而是修改了在该属性中找到的基础结构的值。
这意味着所有共享A原型的对象实际上共享A的实现。这意味着A中携带的任何状态都将在B的所有实例中找到。当您对B的某个实例上的该属性进行赋值时,您已经有效地替换了该实例(仅限该实例)指向的内容(我认为这是因为在到达A的“name”实现之前,在作用域链中命中了B上的新属性“name”)。
编辑:
为了更明确地说明发生了什么,请看以下例子:
B.prototype = new A();
var obj1 = new B();

现在,当我们第一次读取时,解释器会执行以下操作:

obj1.name;

解释器:"我需要属性名称。首先,检查B。B没有'name',因此让我们继续沿作用域链向下查找。接下来是A。啊哈!A有'name',返回它"

然而,当进行写操作时,解释器实际上并不关心继承的属性。

obj1.name = "Fred";

解释器:“我需要给属性‘name’赋值。我在B类实例的作用域内,所以将‘Fred’赋给B。但是我不会影响作用域链中更深层次的其他内容(例如A)。”

现在,下一次你进行读取操作时……

obj1.name;

解释器:“我需要‘name’属性。啊哈!这个B的实例已经有‘name’属性了,就直接返回它——我不关心作用域链上(即A.name)的其他部分。”

因此,第一次写入name时,它会将其插入到实例的属性中,并且不再关心A中的内容。A.name仍然存在,只不过在作用域链的更下面,JS解释器在找到所需内容之前无法到达那里。

如果“name”在A中有默认值(就像你现在使用的“A”),那么你会看到以下行为:

B.prototype = new A();
var obj1 = new B();
var obj2 = new B();

obj1.name = "Fred";

alert(obj1.name); // Alerts Fred
alert(obj2.name); // Alerts "A", your original value of A.name

现在,就你的数组而言,你从未用一个新数组替换了作用域链上的“list”变量。实际上,你只是拿到了数组本身的引用,并向其中添加了一个元素。因此,所有使用B的实例都会受到影响。

解释器:“我需要获取‘list’属性,并向其中添加一个元素。检查这个B实例……没有,它没有‘list’属性。接下来是作用域链:A。是的,A有‘list’,我们用那个。现在,向那个列表中添加元素。”

如果你做了以下操作,情况就不同了:

obj1.list = [];
obj1.push(123);

感谢大家的解释。所以,即使它说:B.prototype = new A(); 和 obj2 = new B();A()的“list”属性并没有被重新创建,只是被回收利用了?无论如何,我可以接受这个命运 :) 那么,有什么最好的方法来拥有一个包含数组的基类?我正在构建一棵树,需要一个通用的带子节点的节点类。 - xgdty

4
当然,我希望任何后续的子类实例都不会从其他实例中获取值,但如果会发生这种情况,那么应该保持一致!那么就不要从一个新的A实例继承,而是从A的原型继承。这将确保您不会继承状态,只会继承行为。这是原型继承中的棘手问题,它也继承了状态,而不仅仅是经典OOP继承中的行为。在JavaScript中,构造函数应仅用于设置实例的状态。
var A = function (list) {
    this.list = list;
    this.name = "A";
};

A.prototype.getName = function () {
    return this.name;
};

var B = function (list) {
    this.list = list;
    this.name = "B";
};

B.prototype = A.prototype;   // Inherit from A's prototype
B.prototype.constructor = B; // Restore constructor object

var b = new B([1, 2, 3]);

// getName() is inherited from A's prototype
print(b.getName()); // B

顺便提一下,如果您想通过B来更改A中的某些内容,您需要执行以下操作:
B.prototype.name = "obj1";

4
  • 您的意思是在这段代码中,B是A的一种实现。
  • 然后你说,obj1是B的一个新实例,所以应该获取所有A的值。
  • 然后你在obj2引用的列表位置添加了一个元素,从而在A的引用位置添加了一个元素(因为它们引用的是同一个东西)。
  • 然后你改变了obj1中名称的值。
  • 然后你说,obj2是B的一个新实例,所以应该获取所有A的值。

您更改了A引用的列表的值,但没有更改引用本身。obj2.name应该是"A"。


3

当你将一个新对象推入obj1.list时,你正在改变原型上现有的列表。当你更改名称时,你正在为实例obj1分配一个属性,而不是原型。请注意,如果你这样做了:

obj1.list = ["from obj1"]
...
console.log(obj2.list) // <--- Will log [] to console

2

您正在重新分配名称变量。发生的是,您正在将一个新的名称变量重新分配给obj1。您对名称的声明覆盖了原型对象上的声明(因此您看不到它)。对于列表,您正在原地改变列表而不是更改其引用。


0

这种行为发生是因为你没有给“list”赋值,而是修改了它。

当你意识到原型链的工作方式时,这种行为是完全合理的。当你引用obj1.list时,它首先查找obj1中是否存在“list”,如果找到则使用该值,否则使用在obj1.prototype(即MyClass.prototype.list)中找到的值。

所以:

obj1.list.push("test"); // modifies MyClass.prototype.list
obj1.list = ["new"]; // creates a "list" property on obj1
obj1.list.push("test"); // modifies obj1.list, not MyClass.prototype.list
delete obj1.list; // removes the "list" property from obj1
// after the delete, obj1.list will point to the prototype again
obj1.list.push("test"); // again modifies MyClass.prototype.list

最重要的一点是:“原型不是类”。原型可以相当好地模拟类,但它们并不是类,因此您不应该将它们视为类。

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