Entendiendo el prototipo
¿Qué hace Object.create()?
La incorporación de Object.create()
a JavaScript permite aún mayor flexibilidad para tratar con objetos. La idea básica es crear objetos a partir del prototipo de otro objeto. Aunque la forma clásica de crear objetos usando una función constructora, la palabra reservada this
para declarar propiedades y el operador new
para las instanciaciones sigue vigente. ¿En qué debemos basarnos para seguir con la forma clásica o usando Object.create
? La verdad aún no sabría decir algo sobre esto. Creo que lo primero es intentar entender el prototipo, pieza angular de los objetos en JavaScript. Veámos primero estos cuatro ejemplos que hacen uso de Object.create()
:
Ejemplo:
En el contenedor anterior hay un elemento <pre id="mensajes1">
que recoge los mensajes de este codigo:
Estas son capturas de pantallas de un Developer Tools después de la ejecucición de cada uno de los ejemplos anteriores:
{ }
se construye tomando como prototipo el objeto íntrinseco Object.Primero tenemos un objeto literal llamado miObjeto
. Cuando JavaScript encuentra un objeto literal entre llaves { }
construye un nuevo objeto tomando como prototipo el objeto intrínseco (o built-in) Object. En la primera captura de pantalla podemos ver como __proto__
es este objeto intrínseco. La propiedad __proto__
es el prototipo y es a su vez también un objeto. Por ejemplo, podemos ver accediendo a la propiedad constructor
de ese objeto __proto__
. La utilidad es que __proto__
es una referencia al verdadero objeto del prototipo, que suele escribirse como [[Prototype]]
y al cual no podemos acceder. Aunque podemos leer y modificar __proto__
se aconseja no usarla, pues al no ser estándar no se comporta igual en todos los navegadores y algunos no la soportan. Además tiene problemas acerca del rendimiento. Muchos defienden el uso de __proto__
basándose en la facilidad para crear objetos. Veámos este ejemplo:
Ejemplo:
Este es el código:
Por lo tanto es lo mismo usar Object.create()
para el mismo propósito. Además hay que comprobar que el navegador soporta __proto__
como puede observar en el código.
Siguiendo con el primer ejemplo pasamos a la segunda captura de pantalla donde creamos var miInstancia
a partir de Object.create(miObjeto)
. Las propiedades nombre
y propiedad
son las del prototipo de miObjeto
. En la siguiente imagen vemos otra instancia miInstancia2
del mismo prototipo de miObjeto
. Ahora modificamos el nombre pero realmente lo que estamos haciendo es agregarlo como una nueva propiedad de la instancia. Cualquier acceso a leer o sobreescribir nombre
en miInstancia2
lo hará en esta instancia. A no ser que borremos la propiedad con delete miInstancia2.nombre
, en cuyo caso los accesos posteriores utilizarían la propiedad nombre
que sigue existiendo en el prototipo miObjeto
.
Cuarta imagen de la serie: ¿Y qué pasa si usamos el operador new
para construir instancias en funciones?, que no sea por probar. Hacemos var miInstancia3 = new Object(miObjeto)
. En este caso el prototipo NO es miObjeto
, sino que apunta al intrínseco Object. Podemos aplicar el operador new
porque Object tiene el método constructor: function Object()
. Es decir, Object()
es una función y por tanto con new Object(miObjeto)
"deberíamos" crear una nueva instancia de Object aplicando como prototipo miObjeto
. Pero al final lo que realmente sucede es que estamos creando una referencia a miObjeto
. Lo mismo que si hubiésemos hecho var miInstancia3 = miObjeto
. Vea que desde miInstancia3
hemos cambiado la propiedad nombre
a miObjeto
, porque realmente miInstancia3 === miObjeto
.
La especificación ECMA-262-5.1 en su apartado 15.2.2.1 new Object([value]) dice que si el argumento, que es opcional, es un objeto de usuario (host object) entonces devuelve algo que depende de la implementación. Pero en los navegadores que he consultado se comporta como si el argumento fuera un objeto intrínseco, donde especifica que no se creará un nuevo objeto sino simplemente devolverá el valor. Vamos, que al final el operador new
en estos casos devuelve el mismo objeto. Sólo tiene utilidad cuando el argumento es nulo, en cuyo caso var x = new Object()
nos devuelve un Object vacío. Es lo mismo que asignarlo con var x = {}
.
En la última imagen vemos una cadena de prototipos. El objeto miInstancia4
se creó con Object.create(miInstancia, ...)
a partir de miInstancia
. Por lo tanto éste es su prototipo, que a su vez tiene como prototipo miObjeto
, que a su vez tiene como prototipo Object. En la construcción de miInstancia4
hemos pasado nuevas propiedades en el segundo argumento del constructor como un objeto literal. Esta es otra ventaja adicional que yo veo con Object.create()
pues permite aplicar parámetros writable
o enumerable
a las propiedades, tal como hacíamos con defineProperties
en el primer tema.
Cómo funciona Object.create()
IE8 no soporta Object.create()
pero el primer ejemplo funciona para este navegador porque, al igual que en el primer tema hemos acompañado una solución:
if (!Object.create){
Object.create = function(proto){
function temp(){};
temp.prototype = proto;
return new temp();
};
}
En el fondo esto explica lo que hace JavaScript con Object.create()
. Al hacer algo como var miInstancia = Object.create(miObjeto)
toma miObjeto
como un prototipo, pues el prototipo no es otra cosa que un objeto. Crea una función temporal para aplicar ese prototipo a la propiedad prototype
y luego instancia una nueva función devolviéndola. Aquí nos encontramos con algo nuevo: la propiedad prototype
en objetos Function. ¿Tiene algo que ver con __proto__
?, es decir, ¿Es prototype
una referencia al interno y no accesible [[Prototype]]
?. ¿Qué es prototype
?
Hagamos un ejemplo
Ejemplo:
Este es el código en forma resumida:
He tomado capturas de pantalla en cada una de las acciones ejecutadas en el código anterior:
__proto__
que es una referencia al interno [[Prototype]]
- Primero declaramos un objeto literal con
var unObjeto = {...}
que, como vimos en el primer apartado, obtiene su prototipo desde[[Prototype]]
que vemos externamente referenciado en la propiedad__proto__
. Le hemos puestos tres propiedadesa,b,c
y un métodover()
. - El siguiente paso lo hacemos con
function unaFuncion(){}
generando una función vacía. Tiene también un__proto__
y la propiedadprototype
apuntando a la misma función. ¿Entonces qué esprototype
? ¿Una función? - Si desplegamos estas dos cosas veremos que
__proto__
apunta afunction Empty(){}
, una función vacía que le sirve de prototipo. Y ésta a su vez tiene otro__proto__
que apunta a Object. Por otro ladoprototype
parece apuntar a sí misma, es decir, aunaFuncion
, con el__proto__
apuntando a Object. Y aunqueunaFuncion
es del tipo Function sucede queunaFuncion.prototype
es del tipo Object. - Luego con
unaFuncion.prototype = unObjeto
asignamos nuestro objeto alprototype
deunaFuncion
sobrescribiendo el que tenía al ser construida. Esto lo podemos ver al consultar de nuevounaFuncion
donde ahoraprototype
es nuestro objeto particular con las propiedadesa,b,c
y el métodover()
. El prototipo interno__proto__
sigue igual que antes apuntando a la función vacía. - En el último paso hacemos
var unObjetoFuncion = new unaFuncion()
y observamos que el__proto__
de esta instancia fue tomado desdeprototype
deunaFunción
y no desde su__proto__
. AhoraunObjetoFuncion
es un objeto copiado o clonado a partir delprototype
de la función.
En resumen, Object.create()
viene a condensar las tres sentencias que vimos en el código que sirve para hacerlo funcionar en navegadores que no soporten este método.
Creando objetos con funciones constructoras
En los ejemplos anteriores partíamos de un objeto literal que nos servía como "plantilla" o prototipo para crear nuevos objetos. Pero ya sabemos que además podemos crearlos usando una función constructora. Como hicimos en ejemplos del tema anterior, usamos la palabra reservada this
para especificar que la declaración de una variable dentro de una función se convertirá en una propiedad del nuevo objeto instanciado con new
. Un ejemplo y unas capturas de pantalla nos ayudará a verlo mejor:
Ejemplo:
Este es el código:
__proto__
que apunta a una función vacía y de un prototype
que apunta a la misma función. Este prototype
, que es un objeto, tiene una propiedad constructor
que es el encargado de construir objetos.Dentro de la función que hemos llamado unConstructor
hemos declarado una propiedad usando this.propiedad = "A"
. Esta sentencia no es una declaración de variable y por tanto no aparece en la imagen primera. Pero está almacenada dentro de la función y servirá para crear las nuevas propiedades del objeto cuando se construya. El método ver()
también lo podíamos haber puesto ahí con this.ver = function(){...}
, pero como vimos en un tema anterior es más eficiente incorporarlo en el prototipo de la función. La función tiene __proto__
que apunta a una función vacía. Y además tiene una propiedad prototype
que apunta a sí misma. En este objeto prototype
encontramos la propiedad constructor
que también está apuntando a sí misma. Este constructor
será el encargado de construir el objeto cuando lo instanciemos aplicando el operador new
.
En la segunda imagen ya tenemos la instanciación realizada con var instancia = new unConstructor()
. Como los objetos que instanciábamos en ejemplos anteriores, tiene su __proto__
que apunta al [[Prototype]]
interno. Ahí es donde se pusieron los métodos como ver()
asignado con prototype
. Este __proto__
heredó también la propiedad constructor
. Por último se nos podría ocurrir instanciar nuevos objetos a partir del constructor de una instancia, pero usando __proto__
, con algo como var instancia2 = new instancia.__proto__.constructor()
. Podría ser necesario si desconocemos cual es la función que construyó el objeto y deseáramos por ejemplo crear una nueva instancia.
En resumen, varias posibilidades para lograr el mismo propósito de crear objetos en JavaScript. Algunas consideraciones sobre el rendimiento de una u otra opción podrían ser tenidas en cuenta si interviene un proceso complejo con una elevada creación de objetos. Pero sea de una forma u otra lo que es evidente es que hemos de tratar de entender como funciona el prototipo.