26º julio 2020

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.

__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])] (mdn:js/Object/create): crea un objeto vacío con el “proto” dado como [[Prototype]] y descriptores de propiedad opcionales.
  • Object.getPrototypeOf(obj) – devuelve el [[Prototype]] de obj.
  • Object.setPrototypeOf(obj, proto) – establece el [[Prototype]] de obj en proto.

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:

// // clon superficial de obj totalmente idéntico
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 y Object.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.

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 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,adios

Resumen

Los métodos modernos para configurar y acceder directamente al prototipo son:

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:

Todos los métodos que devuelven propiedades de objeto (como Object.keys y otros) – devuelven propiedades “propias”. Si queremos heredados, podemos usar for..in.

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", despues "__proto__"
}

// tu toString en accion
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 funcion
      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

Comentarios

lea esto antes de comentar…
  • Si tiene sugerencias sobre qué mejorar, por favor enviar una propuesta de GitHub o una solicitud de extracción en lugar de comentar.
  • Si no puede entender algo en el artículo, por favor explique.
  • Para insertar algunas palabras de código, use la etiqueta <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…)