23 de septiembre de 2022

Métodos prototipo, objetos sin __proto__

En el primer capítulo de esta sección mencionamos que existen métodos modernos para configurar un prototipo.

Leer y escribir en __proto__ se considera desactualizado y algo obsoleto (fue movido al llamado “Anexo B” del estándar JavaScript, dedicado únicamente a navegadores).

Los métodos modernos para obtener y establecer (get/set) un prototipo son:

El único uso de __proto__ que no está mal visto, es como una propiedad cuando se crea un nuevo objeto: { __proto__: ... }.

Aunque hay un método especial para esto también:

Por ejemplo:

let animal = {
  eats: true
};

// crear un nuevo objeto con animal como prototipo
let rabbit = Object.create(animal); // lo mismo que {__proto__: animal}

alert(rabbit.eats); // true

alert(Object.getPrototypeOf(rabbit) === animal); // true

Object.setPrototypeOf(rabbit, {}); // cambia el prototipo de rabbit a {}

El método Object.create es más potente, 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

Hay muchas formas de administrar [[Prototype]]. ¿Cómo pasó esto? ¿Por qué?

Las razones son históricas.

La herencia prototípica estuvo en el lenguaje desde sus albores, pero la manera de manejarla evolucionó con el tiempo.

  • 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. Este le dio la capacidad de crear objetos con un prototipo dado, pero no proporcionaba la capacidad de obtenerlo ni establecerlo. Algunos navegadores implementaron el accessor __proto__ fuera del estándar, lo que permitía obtener/establecer un prototipo en cualquier momento, dando más flexibilidad al desarrollador.
  • Más tarde, en el año 2015, Object.setPrototypeOf y Object.getPrototypeOf se agregaron al estándar para realizar la misma funcionalidad que __proto__ daba. Como __proto__ se implementó de facto en todas partes, fue considerado obsoleto pero logró hacerse camino al Anexo B de la norma, es decir: opcional para entornos que no son del navegador.
  • Más tarde, en el año 2022, fue oficialmente permitido el uso de __proto__ en objetos literales {...} (y movido fuera del Anexo B), pero no como getter/setter obj.__proto__ (sigue en el Anexo B).

¿Por qué se reemplazó __proto__ por las funciones getPrototypeOf/setPrototypeOf?

¿Por qué __proto__ fue parcialmente rehabilitado y su uso permitido en {...}, pero no como getter/setter?

Esa es una pregunta interesante, que requiere que comprendamos por qué __proto__ es malo.

Y pronto llegaremos a la respuesta.

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 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 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 un string 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 terribles. Pero en otros casos podemos estar asignando objetos en lugar de strings, 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.

¿Cómo podemos evitar este problema?

Primero, podemos elegir usar Map para almacenamiento en lugar de objetos simples, entonces todo quedará 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" (tal como se pretende)

… pero la sintaxis con ‘Objeto’ es a menudo más atractiva, por ser más consisa.

Afortunadamente podemos usar objetos, porque 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í.

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);
// o: obj = { __proto__: 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 los métodos nativos que los objetos integrados sí tienen, p.ej. toString:

let obj = Object.create(null);

alert(obj); // Error (no hay toString)

…Pero eso generalmente está bien para arreglos asociativos.

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

  • Para crear un objeto con un prototipo dado, use:

    El Object.create brinda una forma fácil de hacer la 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:

  • No está recomendado 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 son usados como diccionarios, para almacenar cualquier (posiblemente generadas por el usuario) clave.

    Normalmente, los objetos heredan métodos nativos y getter/setter de __proto__ desde Object.prototype, haciendo sus claves correspondientes “ocupadas” y potencialmente causar efectos secundarios. Con el prototipo null, los objetos están verdaderamente vacíos.

Tareas

importancia: 5

Hay un objeto dictionary, creado como Object.create(null), para almacenar cualquier par clave/valor.

Agrega el método dictionary.toString(), que debería devolver una lista de claves delimitadas por comas. Tu toString no debe aparecer al iterar un for..in sobre el objeto.

Así es como debería funcionar:

let dictionary = Object.create(null);

// tu código para agregar el método dictionary.toString

// agregar algunos datos
dictionary.apple = "Manzana";
dictionary.__proto__ = "prueba"; // // aquí proto es una propiedad clave común

// solo manzana y __proto__ están en el ciclo
for(let key in dictionary) {
  alert(key); // "manzana", después "__proto__"
}

// tu toString en acción
alert(dictionary); // "manzana,__proto__"

El método puede tomar todas las claves enumerables usando Object.keys y generar su lista.

Para hacer que toString no sea enumerable, definámoslo usando un descriptor de propiedad. La sintaxis de Object.create nos permite proporcionar un objeto con descriptores de propiedad como segundo argumento.

let dictionary = Object.create(null, {
  toString: { // define la propiedad toString
    value() { // el valor es una función
      return Object.keys(this).join();
    }
  }
});

dictionary.apple = "Manzana";
dictionary.__proto__ = "prueba";

// manzana y __proto__ están en el ciclo
for(let key in dictionary) {
  alert(key); // "manzana", despues "__proto__"
}

// lista de propiedades separadas por comas por toString
alert(dictionary); // "manzana,__proto__"

Cuando creamos una propiedad usando un descriptor, sus banderas son false por defecto. Entonces, en el código anterior, dictionary.toString no es enumerable.

Consulte el capítulo Indicadores y descriptores de propiedad para su revisión.

importancia: 5

Creemos un nuevo objeto rabbit:

function Rabbit(name) {
  this.name = name;
}
Rabbit.prototype.sayHi = function() {
  alert(this.name);
};

let rabbit = new Rabbit("Conejo");

Estas llamadas hacen lo mismo o no?

rabbit.sayHi();
Rabbit.prototype.sayHi();
Object.getPrototypeOf(rabbit).sayHi();
rabbit.__proto__.sayHi();

La primera llamada tiene this == rabbit, las otras tienen this igual a Rabbit.prototype, porque en realidad es el objeto antes del punto.

Entonces, solo la primera llamada muestra Rabbit, las otras muestran undefined:

function Rabbit(name) {
  this.name = name;
}
Rabbit.prototype.sayHi = function() {
  alert( this.name );
}

let rabbit = new Rabbit("Conejo");

rabbit.sayHi();                        // Conejo
Rabbit.prototype.sayHi();              // undefined
Object.getPrototypeOf(rabbit).sayHi(); // undefined
rabbit.__proto__.sayHi();              // undefined
Mapa del Tutorial