Reputation: 351
So I would like to say I understand JavaScript fairly well, but it seems that the language always likes to throw a wrench at me. I have a problem (well maybe not problem, but for sure confusion) on the behavior of "instance" variables for JS objects. Lets take a look at the following code
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);
If you run this code, you will see that one.b
and two.b
have shared the same b
"instance" variable array. I have only noticed this happening on arrays. I understand that if this was A.prototype.b = []
, that it would definitely be shared across all instances of A
or its children. What I don't understand is that it is defined with 'this' command, which means it should be per instance. The interesting thing is that if you uncomment the A.apply(this, arguments)
(effectively calling super on the parent) and remove this.a = a
inside the B
constructor, it works as you think it would. Would anyone know the reason why this is happening?
Upvotes: 3
Views: 62
Reputation: 2873
Part of the problem here has to do with the prototype
property of Functions, versus the [[Prototype]]
hidden property of objects. I'm not sure if any implementations actually use that name for the hidden property -they don't have to, because it's hidden- but some implementations use the non-standard name __proto__
to make it accessible to the script code. I'm going to use this name, even though it's non-standard, to make the distinction clearer between the two; you'll see why in a moment.
The new
operator does a couple of things here. First, it creates a new object and sets its __proto__
to the prototype
property of the function that it's about to call. Then it calls that function, with the new object set to this
.
In other words, you didn't define this.b
with the this
keyword. You defined it all the way back when you created B.prototype
. The result of creating your object one
looks something like this:
{
"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 */
}
See the extra a
and b
on your __proto__
? Those were created when you called new A()
to define B.prototype
. The a
isn't likely to do much of anything, because it's behind another a
in the prototype chain, so things seem to work well enough (most of the time). But that b
is another matter, because it isn't behind anything.
When your call to B.prototype.append
references this.b
, it first looks at the root level of the object for a b
property, and it doesn't find one (because you never defined it). But your object has a __proto__
, so it looks there for a b
: this is called the prototype chain. And it finds one, so it appends to that array. Because all the objects you create with new B()
share a reference to that same object, this gets shared among all the different members.
Let's swap the comments around the way you mentioned. What does the object look like now?
{
"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 */
}
Do you see the difference? Because A()
actually got a chance to run on this
during the call to new B()
, it put a b
at the root of the object, and this one belongs to only that object: it's not part of any __proto__
or anything. You've still got that shared b
hanging back in the prototype chain, but it's got another b
in front of it now, so B.prototype.append()
will never see it. It'll find the instance-specific b
first, and that's why your second example works the way you intended.
One last point. Those shared a
and b
properties back in the prototype chain aren't really doing you any good: in this particular code, they don't do anything but take up space. It IS possible for code to make use of them, but that doesn't suit your use case, so it would be best to get rid of them.
How do you do that? You have to do it without actually calling A()
, because that would set the a
and b
properties back in the prototype chain, and you don't want that, but this means you can't use new A()
. But there's another way to create objects that does what you want: the Object.create
function. You pass it the object that you want to become the __proto__
of the new object, so it looks like this:
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
I usually wrap this rigamarole up in a function, like so:
Object.subclass = function (sub, sup) {
sub.prototype = Object.create(sup.prototype);
sub.prototype.constructor = sub;
}
Once I've got that, I can just call Object.subclass(B, A)
, which is a pretty clear representation of what I want. It does mean that I have to call A()
in the constructor for B()
, just like you describe, but the resulting object doesn't have the extra junk from calling new A()
earlier in the process.
Upvotes: 1
Reputation: 816750
You are doing B.prototype = new A();
which is equivalent to
B.prototype.a = undefined;
B.prototype.b = [];
// or actually
// B.prototype = {a: undefined, b: []};
That's where the shared array comes from.
You are not calling the A
constructor for each instance of B
. Uncomment the A.apply(this, arguments);
line and it will work as expected.
See Benefits of using `Object.create` for inheritance for a better way to setup inheritance.
Upvotes: 3