2012-04-12 8 views
12

He estado leyendo "Javascript: The Good Parts" de Douglas Crockford, y si bien es un poco extremo, estoy de acuerdo con mucho de lo que tiene que decir.Herencia Prototypal de Crockford - Problemas con objetos anidados

En el capítulo 3, analiza los objetos y en un punto establece un patrón (también found here) para simplificar & evitando algunas de las cuestiones/confusión que vienen con el uso de la palabra clave "nueva" incorporada.

if (typeof Object.create !== 'function') { 
    Object.create = function (o) { 
     function F() {} 
     F.prototype = o; 
     return new F(); 
    }; 
} 
newObject = Object.create(oldObject); 

así que he intentado usar esto en un proyecto que estoy trabajando, y me di cuenta de un problema cuando se trata de heredar de objetos que están anidados. Si sobreescribo un valor de un objeto anidado heredado usando este patrón, sobrescribe el elemento anidado hasta el final de la cadena del prototipo.

El ejemplo de Crockford es como el flatObj en el siguiente ejemplo, que funciona bien. El comportamiento, sin embargo, es incompatible con objetos anidados:

var flatObj = { 
    firstname: "John", 
    lastname: "Doe", 
    age: 23 
} 
var person1 = Object.create(flatObj); 

var nestObj = { 
    sex: "female", 
    info: { 
     firstname: "Jane", 
     lastname: "Dough", 
     age: 32 
    } 
} 
var person2 = Object.create(nestObj); 

var nestObj2 = { 
    sex: "male", 
    info: { 
     firstname: "Arnold", 
     lastname: "Schwarzenneger", 
     age: 61 
    } 
} 
var person3 = { 
    sex: "male" 
} 
person3.info = Object.create(nestObj2.info); 

// now change the objects: 
person1.age = 69; 
person2.info.age = 96; 
person3.info.age = 0; 

// prototypes should not have changed: 
flatObj.age // 23 
nestObj.info.age // 96 ??? 
nestObj2.info.age // 61 

// now delete properties: 
delete person1.age; 
delete person2.info.age; 
delete person3.info.age; 

// prototypes should not have changed: 
flatObj.age // 23 
nestObj.info.age // undefined ??? 
nestObj2.info.age // 61 

(también en un fiddle)

¿Estoy haciendo algo mal, o se trata de una limitación de este patrón?

+0

relacionados: [JavaScript Object.create - heredar propiedades anidadas] (http://stackoverflow.com/q/3191103/1048572) – Bergi

Respuesta

10

No hay inconsistencia. Simplemente no piense en objetos anidados: una propiedad directa de un objeto siempre está en su prototipo o en una propiedad propia. Es irrelevante si la propiedad valora una primitiva o un objeto.

Por lo tanto, cuando se hace

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

child.x habrá referencia al mismo objeto que parent.x - que uno {a:0} objeto. Y cuando cambie una propiedad del mismo:

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

ambos se verán afectados. Para cambiar una propiedad "anidado" de forma independiente, primero tendrá que crear un objeto independiente:

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

Lo que puede hacer es

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 

que significa que no son absolutamente independientes - pero todavía dos diferentes objetos.

+0

Esto aclara las cosas bastante - y entiendo el problema ahora. Gracias :) porque todos los objetos son referencias ... Duh. Aún menos conveniente de esta manera. Oh bien. – 1nfiniti

+0

* "una propiedad directa de un objeto siempre está en su prototipo o en una propiedad propia" * - Para mí, una propiedad directa significa propiedad de un objeto, y las que están en su cadena de prototipo podrían llamarse * indirecta *. ¿Puede explicar a qué se refiere exactamente por propiedad directa, o qué es una propiedad * indirecta ...? –

+0

@TJ Creo que usé el término "directo" para decir lo contrario de "anidado" aquí. Un solo nivel en una cadena de propiedades, por así decirlo. – Bergi

1

Creo que lo que sucede es que cuando se crea person2, los sex y info propiedades de las que se refieren a los de nestObj. Cuando hace referencia al person2.info, dado que person2 no redefine la propiedad info, pasa al prototipo y modifica el objeto allí.

Parece que la forma "correcta" de hacerlo es la forma en que compila person3, por lo que el objeto tiene su propio objeto info para modificar y no sube al prototipo.

Estoy leyendo el libro también (despacio), así que simpatizo contigo. :)

1

He cambiado los ejemplos para darle una mejor demostración de lo que está sucediendo aquí.Demo

Primero creamos un objeto con tres propiedades; Un número, una cadena y un objeto con una propiedad con un valor de cadena.

Luego creamos un segundo objeto a partir del primero usando 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" 
} 

Se ve bien ¿verdad? Tenemos nuestro primer objeto y un segundo objeto copiado.

No tan rápido; Veamos qué sucede cuando cambiamos algunos de los valores en el primer objeto.

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" 
} 

Ahora, de nuevo tenemos nuestro primer objeto, con los cambios, y una copia de ese objeto. ¿Que esta pasando aqui?

Vamos a verificar si los objetos tienen sus propias propiedades.

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 tiene todas sus propiedades, al igual que hemos definido, pero obj2 no.

¿Qué sucede cuando cambiamos algunas de las propiedades de obj2?

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" 

Así, num y str cambió el obj2 y no en obj1 al igual que queríamos, pero obj1.obj.less cambiado cuando no debería tener.

De los controles hasOwnProperty() podemos ver que, aunque hemos cambiado obj2.obj.less, no hemos configurado obj2.obj primero. Esto significa que todavía nos estamos refiriendo al obj1.obj.less.

Vamos a crear un objeto a partir del obj1.obj y asignarlo al obj2.obj y ver si eso nos da lo que estamos buscando.

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" 

Eso es bueno, ahora obj2 tiene su propio obj propiedad. Veamos qué pasa cuando cambiamos obj2.obj.less ahora.

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" 
} 

Así que lo que todo esto nos dice es que, si la propiedad no ha sido cambiado en el objeto creado, cualquier get peticiones al objeto creado para que los bienes serán remitidos al objeto original.

La solicitud set para obj2.obj.less = 'more' desde el bloque de código anterior requiere primero una solicitud get para obj2.obj, que no existe en obj2 en ese punto, por lo que hacia delante para obj1.obj y a su vez obj1.obj.less.

Entonces, finalmente, cuando leemos obj2 de nuevo, todavía no hemos establecido obj2.obj modo que get solicitud será enviada a obj1.obj y devolver el ajuste que habíamos cambiado previamente, causando el efecto que el cambio de una propiedad de los objetos segundo objeto el niño parece cambiar ambos, pero realmente solo está cambiando realmente el primero.


Puede usar esta función para devolver un objeto nuevo completamente separado del original recursivamente.

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" 

Vamos a ver lo que sucede cuando cambiamos algunas variables ahora.

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" 
} 

Todo funciona exactamente de la manera que se esperaba que lo haga.

+0

¿A qué consola hace objetos 'console.log' como ese, sin mostrar las estructuras de herencia? – Bergi

+0

Para 'separateObject' Yo recomendaría' Object.create (Object.getPrototypeOf (obj1)) ' – Bergi

+0

Copié el resultado de la consola de JSBin. Acabo de probar 'Object.getPrototypeOf (obj1)' pero devolvió un objeto vacío. ¿Me estoy perdiendo de algo? I –

Cuestiones relacionadas