将JS原型和OOP与实例变量混淆

时间:2014-06-17 16:01:48

标签: javascript variables prototype instance instance-variables

所以我想说我对JavaScript的了解相当不错,但似乎语言总是喜欢对我施加压力。我对JS对象的“实例”变量的行为有一个问题(也许不是问题,但肯定是混乱)。让我们看看下面的代码

function A(a) {
    this.a = a;  
    this.b = [];
}
function B(a) {
   //A.apply(this, arguments);
    this.a = a
}
B.prototype = new A();
B.prototype.constructor = B;
B.prototype.append = function(b) {
    this.b.push(b);  
};

var one = new B("one");
var two = new B("two");
console.log(one.a);
console.log(two.a);
one.append(10);
console.log(one.b);
console.log(two.b);

如果您运行此代码,您会看到one.btwo.b共享了相同的b“实例”变量数组。我只注意到这种情况发生在阵列上。我理解,如果这是A.prototype.b = [],它肯定会在A或其子项的所有实例中共享。我不明白的是它是用'this'命令定义的,这意味着它应该是每个实例。有趣的是,如果您取消注释A.apply(this, arguments)(在父项上有效调用super)并删除this.a = a构造函数中的B,它就会按照您的想法运行。有人会知道发生这种情况的原因吗?

2 个答案:

答案 0 :(得分:3)

你正在做B.prototype = new A();,相当于

B.prototype.a = undefined;
B.prototype.b = [];
// or actually
// B.prototype = {a: undefined, b: []};

这就是共享阵列的来源。

您没有为A的每个实例调用B构造函数。取消注释A.apply(this, arguments);行,它将按预期工作。

有关设置继承的更好方法,请参阅Benefits of using `Object.create` for inheritance

答案 1 :(得分:1)

这里的部分问题与函数的prototype属性有关,而与对象的[[Prototype]]隐藏属性有关。我不确定是否有任何实现使用该隐藏属性的名称 - 他们不必,因为它是隐藏的 - 但是有些实现使用非标准名称__proto__来使其可以访问脚本代码。我将使用这个名称,即使它是非标准的,也可以使两者之间的区别更加清晰;你会马上看到原因。

new运算符在这里做了几件事。首先,它创建一个新对象,并将其__proto__设置为它将要调用的函数的prototype属性。然后它调用该函数,新对象设置为this

换句话说,您没有使用this.b关键字定义this。您在创建B.prototype时已将其定义为完全定义。创建对象one的结果如下所示:

{
    "a" : "one",
    "__proto__" : {
        // This is a reference to B.prototype, which was created with your call to new A()
        "a" : undefined /* because your call to new A() didn't have any arguments */,
        "b" : [],
        "constructor" : /* reference to B() */,
        "append" : /* reference to the append() function you defined */,
        "__proto__" : undefined /* this would reference A.prototype if you had defined one */
}

查看a上的额外b__proto__?这些是在您致电new A()以定义B.prototype时创建的。 a不太可能做任何事情,因为它落后于原型链中的另一个a,因此事情似乎运作得很好(大部分时间)。但b是另一回事,因为它并不落后于任何事情。

当您调用B.prototype.append引用this.b时,它首先查看b属性的对象的根级别,但它找不到一个(因为您从未定义过)它)。但是您的对象有一个__proto__,所以它看起来就是b:这就是所谓的原型链。它找到一个,所以它附加到该数组。因为您使用new B()创建的所有对象共享对同一对象的引用,所以在所有不同成员之间共享。

让我们按照您提到的方式交换评论。该对象现在看起来像什么?

{
    "a" : "one",
    "b" : [],
    "__proto__" : {
        // This is a reference to B.prototype, which was created with your call to new A()
        "a" : undefined /* because your call to new A() didn't have any arguments */,
        "b" : [],
        "constructor" : /* reference to B() */,
        "append" : /* reference to the append() function you defined */,
        "__proto__" : undefined /* this would reference A.prototype if you had defined one */
}

你看到了区别吗?由于A()在调用this期间实际上有机会在new B()上运行,因此它会在对象的根目录中放置一个b,而这只属于那个对象:它不是任何__proto__或任何东西的一部分。您仍然在原型链中悬挂了共享的b,但现在它前面有另一个b,因此B.prototype.append()将永远不会看到它。它首先会找到特定于实例的b,这就是为什么您的第二个示例按照您的预期方式工作的原因。

最后一点。原型链中的那些共享的ab属性对你没有任何好处:在这个特定的代码中,它们除了占用空间之外什么都不做。代码可以使用它们,但这不适合您的用例,因此最好摆脱它们。

你是怎么做到的?您必须在不实际调用A()的情况下执行此操作,因为这会将ab属性设置回原型链中,而您不希望这样,但这意味着您可以不要使用new A()。但是还有另一种方法来创建符合您需要的对象:Object.create函数。你传递了你想成为新对象的__proto__的对象,所以看起来像这样:

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

我通常将这个rigamarole包装在一个函数中,如下所示:

Object.subclass = function (sub, sup) {
    sub.prototype = Object.create(sup.prototype);
    sub.prototype.constructor = sub;
}

一旦我知道了,我就可以打电话给Object.subclass(B, A),这清楚地表达了我想要的东西。它确实意味着我必须在A()的构造函数中调用B(),就像你描述的那样,但是结果对象在进程的早期调用new A()时没有额外的垃圾