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

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *