preloader

Métodos de Prototipo y Objetos Sin __proto__

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]] de obj.
  • Object.setPrototypeOf(obj, proto) – asigna el [[Prototype]] de obj a proto.

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 el proto 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 y Object.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/setter obj.__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]] de obj (igual que el getter de __proto__).
  • Object.setPrototypeOf(obj, proto) – establece el [[Prototype]] de obj en proto (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.

Related Post