所以我想说我对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.b
和two.b
共享了相同的b
“实例”变量数组。我只注意到这种情况发生在阵列上。我理解,如果这是A.prototype.b = []
,它肯定会在A
或其子项的所有实例中共享。我不明白的是它是用'this'命令定义的,这意味着它应该是每个实例。有趣的是,如果您取消注释A.apply(this, arguments)
(在父项上有效调用super)并删除this.a = a
构造函数中的B
,它就会按照您的想法运行。有人会知道发生这种情况的原因吗?
答案 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
,这就是为什么您的第二个示例按照您的预期方式工作的原因。
最后一点。原型链中的那些共享的a
和b
属性对你没有任何好处:在这个特定的代码中,它们除了占用空间之外什么都不做。代码可以使用它们,但这不适合您的用例,因此最好摆脱它们。
你是怎么做到的?您必须在不实际调用A()
的情况下执行此操作,因为这会将a
和b
属性设置回原型链中,而您不希望这样,但这意味着您可以不要使用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()
时没有额外的垃圾