Crockford's Prototypal inheritance - Issues with nested objects

There is no inconsistency. Just don't think of nested objects: a direct property of an object is always either on its prototype or an own property. It's irrelevant wheter the property value a primitive or an object.

So, when you do

var parent = {
    x: {a:0}
};
var child = Object.create(parent);

child.x will be referencing the same object as parent.x - that one {a:0} object. And when you change a property of it:

var prop_val = child.x; // == parent.x
prop_val.a = 1;

both will be affected. To change a "nested" property independently, you first will have to create an independent object:

child.x = {a:0};
child.x.a = 1;
parent.x.a; // still 0

What you can do is

child.x = Object.create(parent.x);
child.x.a = 1;
delete child.x.a; // (child.x).a == 0, because child.x inherits from parent.x
delete child.x; // (child).x.a == 0, because child inherits from parent

which means they are not absolutely independent - but still two different objects.


I think what's happening is that when you create person2, the sex and info properties of it refer to those in nestObj. When you reference person2.info, since person2 doesn't redefine the info property, it goes through to the prototype and modifies the object there.

It looks like the "right" way to do it is the way you build person3, so that the object has its own info object to modify and doesn't go up to the prototype.

I'm reading the book too (slowly), so I sympathize with you. :)


I've changed the examples to give you a better demonstration of what is happening here. Demo

First we create an object with three properties; A number, a string and an object with one property with a string value.

Then we create a second object from the first using Object.create();

var obj1 = { 
    num : 1,
    str : 'foo',
    obj : { less: 'more' }
};
var obj2 = Object.create( obj1 );

console.log( '[1] obj1:', obj1 );
console.log( '[1] obj2:', obj2 );
"[1] obj1:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "foo"
}
"[1] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "foo"
}

Looks good right? We have our first object and a second copied object.

Not so fast; Let's see what happens when we change some of the values on the first object.

obj1.num = 3;
obj1.str = 'bar';
obj1.obj.less = 'less';

console.log( '[2] obj1:', obj1 );
console.log( '[2] obj2:', obj2 );
"[2] obj1:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "less"
  },
  str: "bar"
}
"[2] obj2:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "less"
  },
  str: "bar"
}

Now again we have our first object, with changes, and a copy of that object. What's happening here?

Let's check if the objects have their own properties.

for( var prop in obj1 ) console.log( '[3] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) );
for( var prop in obj2 ) console.log( '[3] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) );
"[3] obj1.hasOwnProperty( num ): true"
"[3] obj1.hasOwnProperty( str ): true"
"[3] obj1.hasOwnProperty( obj ): true"
"[3] obj2.hasOwnProperty( num ): false"
"[3] obj2.hasOwnProperty( str ): false"
"[3] obj2.hasOwnProperty( obj ): false"

obj1 has all of its own properties, just like we defined, but obj2 doesn't.

What happens when we change some of obj2's properties?

obj2.num = 1;
obj2.str = 'baz';
obj2.obj.less = 'more';

console.log( '[4] obj1:', obj1 );
console.log( '[4] obj2:', obj2 );
for( var prop in obj1 ) console.log( '[4] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) );
for( var prop in obj2 ) console.log( '[4] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) );
"[4] obj1:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "more"
  },
  str: "bar"
}
"[4] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "baz"
}
"[4] obj1.hasOwnProperty( num ): true"
"[4] obj1.hasOwnProperty( str ): true"
"[4] obj1.hasOwnProperty( obj ): true"
"[4] obj2.hasOwnProperty( num ): true"
"[4] obj2.hasOwnProperty( str ): true"
"[4] obj2.hasOwnProperty( obj ): false"

So, num and str changed on obj2 and not on obj1 just like we wanted, but obj1.obj.less changed when it shouldn't have.

From the hasOwnProperty() checks we can see that, even though we changed obj2.obj.less, we didn't set obj2.obj first. This means that we are still referring to obj1.obj.less.

Let's create an object from obj1.obj and assign it to obj2.obj and see if that gives us what we're looking for.

obj2.obj = Object.create( obj1.obj );

console.log( '[5] obj1:', obj1 );
console.log( '[5] obj2:', obj2 );
for( var prop in obj1 ) console.log( '[5] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) );
for( var prop in obj2 ) console.log( '[5] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) );
"[5] obj1:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "more"
  },
  str: "bar"
}
"[5] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "baz"
}
"[5] obj1.hasOwnProperty( num ): true"
"[5] obj1.hasOwnProperty( str ): true"
"[5] obj1.hasOwnProperty( obj ): true"
"[5] obj2.hasOwnProperty( num ): true"
"[5] obj2.hasOwnProperty( str ): true"
"[5] obj2.hasOwnProperty( obj ): true"

That's good, now obj2 has its own obj property. Let's see what happens when we change obj2.obj.less now.

obj2.obj.less = 'less';

console.log( '[6] obj1:', obj1 );
console.log( '[6] obj2:', obj2 );
"[6] obj1:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "more"
  },
  str: "bar"
}
"[6] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "less"
  },
  str: "baz"
}

So what this all tells us is that, if the property has not yet been changed on the created object, any get requests to the created object for that property will be forwarded to the original object.

The set request for obj2.obj.less = 'more' from the previous code block first requires a get request for obj2.obj, which doesn't exist in obj2 at that point, so it forwards to obj1.obj and in turn obj1.obj.less.

Then finally when we read obj2 again, we still haven't set obj2.obj so that get request will be forwarded to obj1.obj and return the setting that we had previously changed, causing the effect that changing a property of the second objects object child seems to change both, but really it is only actually changing the first.


You can use this function to return a new object completely separated from the original recursively.

Demo

var obj1 = { 
    num : 1,
    str : 'foo',
    obj : { less: 'more' }
};
var obj2 = separateObject( obj1 );

function separateObject( obj1 ) {

    var obj2 = Object.create( Object.getPrototypeOf( obj1 ) );
    for(var prop in obj1) {
        if( typeof obj1[prop] === "object" )
            obj2[prop] = separateObject( obj1[prop] );
        else
            obj2[prop] = obj1[prop];
    }

    return obj2;
}

console.log( '[1] obj1:', obj1 );
console.log( '[1] obj2:', obj2 );
for( var prop in obj1 ) console.log( '[1] obj1.hasOwnProperty( ' + prop + ' ): ' + obj1.hasOwnProperty( prop ) );
for( var prop in obj2 ) console.log( '[1] obj2.hasOwnProperty( ' + prop + ' ): ' + obj2.hasOwnProperty( prop ) );
"[1] obj1:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "foo"
}
"[1] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "foo"
}
"[1] obj1.hasOwnProperty( num ): true"
"[1] obj1.hasOwnProperty( str ): true"
"[1] obj1.hasOwnProperty( obj ): true"
"[1] obj2.hasOwnProperty( num ): true"
"[1] obj2.hasOwnProperty( str ): true"
"[1] obj2.hasOwnProperty( obj ): true"

Let's see what happens when we change some variables now.

obj1.num = 3;
obj1.str = 'bar';
obj1.obj.less = 'less';

console.log( '[2] obj1:', obj1 );
console.log( '[2] obj2:', obj2 );
"[2] obj1:"
[object Object] {
  num: 3,
  obj: [object Object] {
    less: "less"
  },
  str: "bar"
}
"[2] obj2:"
[object Object] {
  num: 1,
  obj: [object Object] {
    less: "more"
  },
  str: "foo"
}

Everything works exactly the way you expected it to.