En el primer capítulo de esta sección, mencionamos que existen métodos modernos para configurar un prototipo.
__proto__
se considera desactualizado y algo obsoleto (en la parte propia del navegador dentro del estándar JavaScript).
Los métodos modernos son:
- Object.create(proto, [descriptors]) – crea un objeto vacío con el “proto” dado como
[[Prototype]]
y descriptores de propiedad opcionales. - Object.getPrototypeOf(obj) – devuelve el
[[Prototype]]
deobj
. - Object.setPrototypeOf(obj, proto) – establece el
[[Prototype]]
deobj
enproto
.
Estos deben usarse en lugar de __proto__
.
Por ejemplo:
let animal = {
eats: true
};
// crear un nuevo objeto con animal como prototipo
let rabbit = Object.create(animal);
alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // true
Object.setPrototypeOf(rabbit, {}); // cambia el prototipo de rabbit a {}
Object.create
tiene un segundo argumento opcional: descriptores de propiedad. Podemos proporcionar propiedades adicionales al nuevo objeto allí, así:
let animal = {
eats: true
};
let rabbit = Object.create(animal, {
jumps: {
value: true
}
});
alert(rabbit.jumps); // true
Los descriptores están en el mismo formato que se describe en el capítulo Indicadores y descriptores de propiedad.
Podemos usar Object.create
para realizar una clonación de objetos más poderosa que copiar propiedades en el ciclo for..in
:
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
Esta llamada hace una copia verdaderamente exacta de obj
, que incluye todas las propiedades: enumerables y no enumerables, propiedades de datos y setters/getters, todo, y con el [[Prototype]]
correcto.
Breve historia
Si contamos todas las formas de administrar [[Prototype]]
, ¡hay muchas! ¡Muchas maneras de hacer lo mismo!
¿Por qué?
Eso es por razones históricas.
- La propiedad “prototipo” de una función de constructor ha funcionado desde tiempos muy antiguos.
- Más tarde, en el año 2012, apareció
Object.create
en el estándar. Le dio la capacidad de crear objetos con un prototipo dado, pero no proporcionó la capacidad de obtenerlo/configurarlo. Entonces, los navegadores implementaron el acceso no estándar__proto__
que permitió al usuario obtener/configurar un prototipo en cualquier momento. - Más tarde, en el año 2015,
Object.setPrototypeOf
yObject.getPrototypeOf
se agregaron al estándar, para realizar la misma funcionalidad que__proto__
. Como__proto__
se implementó de facto en todas partes, fue desaprobado y llegó al Anexo B de la norma, es decir: opcional para entornos que no son del navegador.
A partir de ahora tenemos todas estas formas a nuestra disposición.
¿Por qué se reemplazó __proto__
por las funciones getPrototypeOf/setPrototypeOf
? Esa es una pregunta interesante, que requiere que comprendamos por qué __proto__
es malo. Sigue leyendo para obtener la respuesta.
[[Prototype]]
en objetos existentes si la velocidad es importanteTécnicamente, podemos obtener/configurar [[Prototype]]
en cualquier momento. Pero generalmente solo lo configuramos una vez en el momento de creación del objeto y ya no lo modificamos: rabbit
hereda de animal
, y eso no va a cambiar.
Y los motores de JavaScript están altamente optimizados para esto. Cambiar un prototipo “sobre la marcha” con Object.setPrototypeOf
u obj.__ proto __=
es una operación muy lenta ya que rompe las optimizaciones internas para las operaciones de acceso a la propiedad del objeto. Por lo tanto, evítelo a menos que sepa lo que está haciendo, o no le importe la velocidad de JavaScript .
Objetos "muy simples"
Como sabemos, los objetos se pueden usar como arreglos asociativas para almacenar pares clave/valor.
…Pero si tratamos de almacenar claves proporcionadas por el usuario en él (por ejemplo, un diccionario ingresado por el usuario), podemos ver una falla interesante: todas las claves funcionan bien excepto "__proto __ "
.
Mira el ejemplo:
let obj = {};
let key = prompt("Cual es la clave?", "__proto__");
obj[key] = "algún valor";
alert(obj[key]); // [object Object], no es "algún valor"!
Aquí, si el usuario escribe en __proto__
, ¡la asignación se ignora!
Eso no debería sorprendernos. La propiedad __proto__
es especial: debe ser un objeto o null
. Una cadena no puede convertirse en un prototipo.
Pero no intentamos implementar tal comportamiento, ¿verdad? Queremos almacenar pares clave/valor, y la clave llamada "__proto__"
no se guardó correctamente. ¡Entonces eso es un error!
Aquí las consecuencias no son terribles. Pero en otros casos podemos estar asignando valores de objeto, y luego el prototipo puede ser cambiado. Como resultado, la ejecución irá mal de maneras totalmente inesperadas.
Lo que es peor: generalmente los desarrolladores no piensan en tal posibilidad en absoluto. Eso hace que tales errores sean difíciles de notar e incluso los convierta en vulnerabilidades, especialmente cuando se usa JavaScript en el lado del servidor.
También pueden ocurrir cosas inesperadas al asignar a toString
, que es una función por defecto, y a otros métodos integrados.
¿Cómo podemos evitar este problema?
Primero, podemos elegir usar Map
para almacenamiento en lugar de objetos simples, luego todo queda bien.
Pero ‘Objeto’ también puede servirnos bien aquí, porque los creadores del lenguaje pensaron en ese problema hace mucho tiempo.
__proto__
no es una propiedad de un objeto, sino una propiedad de acceso de Object.prototype
:
Entonces, si se lee o establece obj.__ proto__
, el getter/setter correspondiente se llama desde su prototipo y obtiene/establece [[Prototype]]
.
Como se dijo al comienzo de esta sección del tutorial: __proto__
es una forma de acceder a [[Prototype]]
, no es [[Prototype]]
en sí.
Ahora, si pretendemos usar un objeto como una arreglo asociativa y no tener tales problemas, podemos hacerlo con un pequeño truco:
let obj = Object.create(null);
let key = prompt("Cual es la clave", "__proto__");
obj[key] = "algún valor";
alert(obj[key]); // "algún valor"
Object.create(null)
crea un objeto vacío sin un prototipo ([[Prototype]]
es null
):
Entonces, no hay getter/setter heredado para __proto__
. Ahora se procesa como una propiedad de datos normal, por lo que el ejemplo anterior funciona correctamente.
Podemos llamar a estos objetos: objetos “muy simples” o “de diccionario puro”, porque son aún más simples que el objeto simple normal {...}
.
Una desventaja es que dichos objetos carecen de métodos de objetos integrados, p.ej. toString
:
let obj = Object.create(null);
alert(obj); // Error (no hay toString)
…Pero eso generalmente está bien para arreglos asociativas.
Tenga en cuenta que la mayoría de los métodos relacionados con objetos son Object.algo(...)
, como Object.keys(obj)
y no están en el prototipo, por lo que seguirán trabajando en dichos objetos:
let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";
alert(Object.keys(chineseDictionary)); // hola, adiós
Resumen
Los métodos modernos para configurar y acceder directamente al prototipo son:
- Object.create(proto, [descriptores]) – crea un objeto vacío con un
proto
dado como[[Prototype]]
(puede sernulo
) y descriptores de propiedad opcionales. - Object.getPrototypeOf(obj) – devuelve el
[[Prototype]]
deobj
(igual que el getter de__proto__
). - Object.setPrototypeOf(obj, proto) – establece el
[[Prototype]]
deobj
enproto
(igual que el setter de__proto__
).
El getter/setter incorporado de __proto__
no es seguro si queremos poner claves generadas por el usuario en un objeto. Aunque un usuario puede ingresar "__proto __"
como clave, y habrá un error, con consecuencias levemente dañinas, pero generalmente impredecibles.
Entonces podemos usar Object.create(null)
para crear un objeto “muy simple” sin __proto__
, o apegarnos a los objetos Map
para eso.
Además, Object.create
proporciona una manera fácil de copiar llanamente un objeto con todos los descriptores:
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
También dejamos en claro que __proto__
es un getter/setter para [[Prototype]]
y reside en Object.prototype
, al igual que otros métodos.
Podemos crear un objeto sin prototipo mediante Object.create(null)
. Dichos objetos se utilizan como “diccionarios puros”, no tienen problemas con "__proto __"
como clave.
Otros métodos:
- Object.keys(obj) / Object.values(obj) / Object.entries(obj): devuelve una arreglo de pares clave-valor: nombres/valores, de propiedades de cadena propios enumerables.
- Object.getOwnPropertySymbols(obj): devuelve un arreglo de todas las claves simbólicas propias.
- Object.getOwnPropertyNames(obj): devuelve un arreglo de todas las claves de cadena propias.
- Reflect.ownKeys(obj): devuelve un arreglo de todas las claves propias.
- obj.hasOwnProperty(key): devuelve
true
siobj
tiene su propia clave (no heredada) llamadakey
.
Todos los métodos que devuelven propiedades de objeto (como Object.keys
y otros) – devuelven propiedades “propias”. Si queremos heredados, podemos usar for..in
.
Comentarios
<code>
, para varias líneas – envolverlas en la etiqueta<pre>
, para más de 10 líneas – utilice una entorno controlado (sandbox) (plnkr, jsbin, codepen…)