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.