Search code examples
javascriptprototypal-inheritance

Prototypal inheritance with nested objects


I'm trying to get my head around prototype inheritance in Javascript. I think I got the basic concept, but when I was playing around with this I ran into the following which still has me puzzled.

There is a very similar question and answer here but it doesn't fully answer why this is happening, at least not for me.

I create a new object like this:

var User = {
  username: "",
  name: {
    first: "",
    last: ""
  }
}

Next I create two "instances" of that object:

var user1 = Object.create(User);
var user2 = Object.create(User);

Now I set the name property like so:

user1.name = { first: "John", last: "Jackson"}
user2.name = { first: "James", last: "Jameson"}

Now I do

alert(user1.name.first) \\ -> John alert(user2.name.first) \\ -> James

All as expected. So far so good.

However, if I set the name.first property like this:

user1.name.first = "John";
user2.name.first = "James";

and I get

alert(user1.name.first) \\ -> James
alert(user2.name.first) \\ -> James

Clearly now the property is being set on the prototype object User (or rather the contained name object) instead of overriding it in the current object user1. Why does that occur?

Further if I do

user1.name.middle = "Mortimer"

I can now do

alert(User.name.middle) // -> Mortimer

which is not what I would expect. Generally, whenever a property is set on a derived object, that object either already has that property as an ownProperty in which case the value is simply assigned, or the property is newly created as an ownProperty on the derived object, overriding the prototype property. Just like happens when I assign to user1.name.

So why does assigning to an object contained in the prototype object cause such (at least to me) unexpected and counter-intuitive behavior?

The way I understand it, when the assignment is made the first check is to see if user1 has an ownProperty called name, which it doesn't. If this were a read operation the prototype property would now be looked up and User checked to see if it has ownProperty name. But since this is a set operation why walk the prototype chain when usually a missing ownProperty is simply created?


Solution

  • But since this is a set operation why walk the prototype chain when usually a missing ownProperty is simply created?

    When you say user1.name.first = "John", the user1.name part has to be resolved before the .first property can be retrieved or set. And in your example the user1.name part only exists on the prototype object, so it is that object whose .first property you are setting.

    Similarly, when you say user1.name.middle = "Mortimer", again the user1.name part resolves to the nested object from the prototype, so then you create a .middle property on that object, which is why User.name.middle also returns "Mortimer".

    If you said user1.name.first and user1.name could not be resolved (on the current object or in its prototype chain) then you'd have a TypeError: Cannot set property 'first' of undefined. (You can try that concept with your existing code by saying user1.address.street = "something" - you'd get the TypeError, because user1.address doesn't exist on user1 or its prototype chain.)