ken
ken

Reputation: 3709

Instanced object and properties that are references

Just when I thought I had JS figured out, I get hung up on this:

function Obj() {
    console.log('x: %s, o.x: %s', this.x++, this.o.x++);
}    
Obj.prototype.x = 1;
Obj.prototype.o = {x: 1};

Expected:

> new Obj
x: 1, o.x: 1
> new Obj
x: 1, o.x: 1
> new Obj
x: 1, o.x: 1

Actual:

> new Obj
x: 1, o.x: 1
> new Obj
x: 1, o.x: 2
> new Obj
x: 1, o.x: 3

So, it seems that if a prototypal property is a reference type, then it is shared across all instances, yet if it's a non-reference type then it is reinitialized for each instance; to confirm this hypothesis, I tested a few other types, such as string (which behaves like number) and array (which behaves like object).

I have determined that I can avoid this pitfall if I reinitialize the object property in the ctor, like this:

function Obj() {
    this.o = {x: 1};
}

Which just seems really unorthodox (that I'd be required to manually reinitialize properties but only if they're a reference object).

Can anyone shed some light on what's going on?

Upvotes: 5

Views: 131

Answers (2)

Erik  Reppen
Erik Reppen

Reputation: 4635

I think the first answer has the gist of it, but I'm answering from a slightly different angle.

The key to understanding it is object behavior in regards to assigning to an object.

Lookups fall back to the prototype if there is no instance property. Always.

Assignments, however, will auto-create a new instance property every time. This won't really matter if you assign a by-reference prototyped property to a new instance property since they both are just pointers to the same thing. And if you mutate in-place, no new instance property will be created either.

function Obj(){
    //this.refArr resolves to what's in the prototype and that's what we change
    this.refArr.push(1);

    //now copy sliceArray and assign a new but identical array
    this.sliceArray = this.sliceArray.slice(0);

    //no length increase would mean a new instance property is created
    //... and then the assigment took place.
    this.sliceArray.push(1);
    console.log( 'ref:'+this.refArr.length +', sliceArray:'+this.sliceArray.length);
}
Obj.prototype.refArr = [];
Obj.prototype.sliceArray = [];
new Obj; new Obj;
//ref:1, sliceArray:1
//ref:2, sliceArray:1

And that's essentially the difference in what you're doing. this.x = this.x + 1 creates a new property. Since you assign to a property of this.constructor.prototype.o no new property is created. If you incremented and then assigned this.o to this.o it wouldn't matter anyway, since prototype and new property version would just point at the same object.

Upvotes: 0

I Hate Lazy
I Hate Lazy

Reputation: 48771

Think of it this way. You're always going to be modifying the object on which you're operating, irrespective of where the value comes from.

When you do this for the first time:

this.x++;

It is getting the value of x from Obj.prototype.x because there was no x property directly on the this object, nevertheless the ++ is still operating on the this object, so the value is set on that object. That value is now directly on the instance for future modifications. (The prototype.x is shadowed until the direct property is deleted.)

However, when you do this:

this.o.x++

The only operation on the this object is the lookup of o. Since again there is no o property on this, you'll get the reference to the object stored at Obj.prototype.o. At this point, there's no actual modification of the this object.

After the reference is returned, you then look up the property x on the Obj.prototype.o object, and modify it.

So it's actually quite consistent. With this.o.x++ you've performed a lookup on this, but no mutation. The mutation is on the referenced object. But with this.x++, you're mutating the object directly.


So yes, unless you want your reference types to be shared among all instances created from the constructor, you should put the reference types directly on the instances and not on the .prototype.

Upvotes: 3

Related Questions