Métodos de Prototipo y Objetos Sin __proto__
En el capítulo inicial de esta sección, mencionamos que existen métodos modernos para manejar prototipos.
El uso de __proto__
para leer y escribir prototipos se considera anticuado y está algo desaconsejado (ha sido trasladado al llamado «Anexo B» del estándar JavaScript, dedicado exclusivamente a los navegadores).
Métodos Modernos para Prototipos
Los métodos actuales para obtener y establecer un prototipo son:
Object.getPrototypeOf(obj)
– devuelve el [[Prototype]] deobj
.Object.setPrototypeOf(obj, proto)
– asigna el [[Prototype]] deobj
aproto
.
El único uso de __proto__
que no está mal visto es como propiedad al crear un nuevo objeto: { __proto__: ... }
.
Sin embargo, también existe un método especial para esto:
Object.create(proto, [descriptors])
– crea un objeto vacío con elproto
especificado como [[Prototype]] y descriptores de propiedad opcionales.
Por ejemplo:
let animal = {
eats: true
};
// crear un nuevo objeto con animal como prototipo
let rabbit = Object.create(animal); // similar a {__proto__: animal}
console.log(rabbit.eats); // true
console.log(Object.getPrototypeOf(rabbit) === animal); // true
Object.setPrototypeOf(rabbit, {}); // cambia el prototipo de rabbit a {}
Usando Object.create
El método Object.create
es más potente, ya que tiene un segundo argumento opcional: los descriptores de propiedad. Podemos añadir propiedades adicionales al nuevo objeto así:
let animal = {
eats: true
};
let rabbit = Object.create(animal, {
jumps: {
value: true
}
});
console.log(rabbit.jumps); // true
Los descriptores están en el mismo formato que se describe en el capítulo sobre Indicadores y Descriptores de Propiedad.
Podemos usar Object.create
para realizar una clonación de objetos más avanzada que copiar propiedades en un ciclo for..in
:
let clone = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
Esta llamada crea una copia exacta de obj
, incluyendo todas las propiedades: enumerables y no enumerables, propiedades de datos y getters/setters, y con el [[Prototype]] correcto.
Breve Historia
Hay muchas formas de manejar [[Prototype]]. ¿Cómo surgió esto? ¿Por qué?
Las razones son históricas.
La herencia prototípica ha estado en el lenguaje desde sus inicios, pero la manera de manejarla ha evolucionado con el tiempo.
- La propiedad «prototype» de una función constructora ha existido desde hace mucho tiempo.
- En 2012, se añadió
Object.create
al estándar, permitiendo crear objetos con un prototipo dado, pero sin proporcionar la capacidad de obtenerlo o establecerlo. Algunos navegadores implementaron el accessor__proto__
fuera del estándar, lo que permitió obtener/establecer un prototipo en cualquier momento, dando más flexibilidad al desarrollador. - En 2015, se añadieron
Object.setPrototypeOf
yObject.getPrototypeOf
al estándar para realizar la misma funcionalidad que__proto__
proporcionaba. Como__proto__
se implementó de facto en todas partes, fue considerado obsoleto pero se incluyó en el Anexo B de la norma, es decir: opcional para entornos que no son del navegador. - En 2022, se permitió oficialmente el uso de
__proto__
en objetos literales{...}
(y se movió fuera del Anexo B), pero no como getter/setterobj.__proto__
(sigue en el Anexo B).
¿Por Qué se Reemplazó __proto__
?
¿Por qué __proto__
fue parcialmente rehabilitado y su uso permitido en {...}
, pero no como getter/setter?
Esa es una pregunta interesante que requiere entender por qué __proto__
es problemático.
No Cambie [[Prototype]] en Objetos Existentes Si la Velocidad es Importante
Técnicamente, podemos obtener/configurar [[Prototype]] en cualquier momento. Pero generalmente solo lo configuramos una vez durante la creación del objeto y no lo cambiamos más: el conejo hereda de animal, y eso no va a cambiar.
Los motores de JavaScript están altamente optimizados para esto. Cambiar un prototipo «sobre la marcha» con Object.setPrototypeOf
o obj.__proto__
es una operación muy lenta ya que rompe las optimizaciones internas para las operaciones de acceso a propiedades 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 pueden usarse como arreglos asociativos para almacenar pares clave/valor.
Pero si intentamos almacenar claves proporcionadas por el usuario en él (por ejemplo, un diccionario ingresado por el usuario), podemos ver un problema interesante: todas las claves funcionan bien excepto __proto__
.
Mire el ejemplo:
let obj = {};
let key = prompt("¿Cuál es la clave?", "__proto__");
obj[key] = "algún valor";
alert(obj[key]); // [object Object], no "algún valor"!
Aquí, si el usuario escribe en __proto__
, ¡la asignación en la línea 4 es ignorada!
Eso no debería sorprendernos. La propiedad __proto__
es especial: debe ser un objeto o null
. Una cadena no puede convertirse en un prototipo. Es por ello que la asignación de una cadena a __proto__
es ignorada.
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 graves. Pero en otros casos podemos estar asignando objetos en lugar de cadenas, y el prototipo efectivamente 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 obj.toString
, por ser un método integrado.
Evitar el Problema
Primero, podemos optar por usar Map
para almacenamiento en lugar de objetos simples, entonces todo funcionará bien.
let map = new Map();
let key = prompt("¿Cuál es la clave?", "__proto__");
map.set(key, "algún valor");
alert(map.get(key)); // "algún valor" (como se pretende)
Pero la sintaxis con ‘Objeto’ es a menudo más atractiva por ser más concisa.
Afortunadamente, podemos usar objetos, ya que los creadores del lenguaje pensaron en ese problema hace mucho tiempo.
Como sabemos, __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í.
Creando Objetos Sin Prototipo
Si pretendemos usar un objeto como un arreglo asociativo y no tener tales problemas, podemos hacerlo con un pequeño truco:
let obj = Object.create(null);
// o: obj = { __proto__: null }
let key = prompt("¿Cuál 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 los métodos nativos que los objetos integrados sí tienen, por ejemplo, toString
:
let obj = Object.create(null);
alert(obj); // Error (no hay toString)
Pero eso generalmente está bien para arreglos asociativos.
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 funcionando en dichos objetos:
let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";
alert(Object.keys(chineseDictionary)); // hello, bye
Resumen
Para crear un objeto con un prototipo dado, use:
- Sintaxis literal:
{ __proto__: ... }
, que permite especificar múltiples propiedades. - O
Object.create(proto, [descriptors])
, que permite especificar descriptores de propiedad.
Object.create
brinda una forma fácil de hacer una copia superficial de un objeto con todos sus descriptores:
let clone = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
Los métodos modernos para obtener y establecer el prototipo son:
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__
).
No se recomienda obtener y establecer el prototipo usando los getter/setter nativos de __proto__
. Ahora están en el Anexo B de la especificación.
También hemos cubierto objetos sin prototipo, creados con Object.create(null)
o {__proto__: null}
.
Estos objetos se usan como diccionarios para almacenar cualquier clave (posiblemente generada por el usuario).
Normalmente, los objetos heredan métodos nativos y getter/setter de __proto__
desde Object.prototype
, haciendo que sus claves correspondientes estén «ocupadas» y potencialmente causen efectos secundarios. Con el prototipo null, los objetos están verdaderamente vacíos.