ZeroHour
ZeroHour

Reputation: 351

Confusing JS prototyping and OOP with instance variables

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

Answers (2)

The Spooniest
The Spooniest

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

Felix Kling
Felix Kling

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

Related Questions