En el primer capítulo de esta sección, mencionamos que existen métodos modernos para configurar un prototipo.
Configurar o leer el prototipo con obj.__proto__
se considera obsoleto y algo obsoleto (se trasladó al llamado "Anexo B" del estándar JavaScript, destinado únicamente a navegadores).
Los métodos modernos para obtener/configurar un prototipo son:
Object.getPrototypeOf(obj): devuelve el [[Prototype]]
de obj
.
Object.setPrototypeOf(obj, proto): establece el [[Prototype]]
de obj
en proto
.
El único uso de __proto__
, que no está mal visto, es como propiedad al crear un nuevo objeto: { __proto__: ... }
.
Aunque también existe un método especial para esto:
Object.create(proto[, descriptores]): crea un objeto vacío con proto
dado como [[Prototype]]
y descriptores de propiedad opcionales.
Por ejemplo:
dejar animal = { come: cierto }; // crea un nuevo objeto con un animal como prototipo let conejo = Object.create(animal); // igual que {__proto__: animal} alerta(conejo.come); // verdadero alerta(Object.getPrototypeOf(conejo) === animal); // verdadero Object.setPrototypeOf(conejo, {}); // cambia el prototipo de conejo a {}
El método Object.create
es un poco más potente, ya que tiene un segundo argumento opcional: descriptores de propiedades.
Podemos proporcionar propiedades adicionales al nuevo objeto allí, como esta:
dejar animal = { come: cierto }; dejar conejo = Objeto.create(animal, { salta: { valor: verdadero } }); alerta(conejo.salta); // verdadero
Los descriptores tienen el mismo formato que se describe en el capítulo Indicadores y descriptores de propiedades.
Podemos usar Object.create
para realizar una clonación de objetos más poderosa que copiar propiedades en for..in
:
dejar clonar = Objeto.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) );
Esta llamada hace una copia verdaderamente exacta de obj
, incluidas todas las propiedades: enumerables y no enumerables, propiedades de datos y definidores/obtendores, todo, y con el [[Prototype]]
correcto.
Hay muchas formas de administrar [[Prototype]]
. ¿Cómo sucedió eso? ¿Por qué?
Eso es por razones históricas.
La herencia prototípica estuvo en la lengua desde sus albores, pero las formas de gestionarla evolucionaron con el tiempo.
La propiedad prototype
de una función constructora ha funcionado desde tiempos muy antiguos. Es la forma más antigua de crear objetos con un prototipo determinado.
Posteriormente, en el año 2012, apareció Object.create
en el estándar. Daba la posibilidad de crear objetos con un prototipo determinado, pero no proporcionaba la posibilidad de obtenerlo/configurarlo. Algunos navegadores implementaron el descriptor de acceso __proto__
no estándar que permitía al usuario obtener/configurar un prototipo en cualquier momento, para brindar más flexibilidad a los desarrolladores.
Posteriormente, en el año 2015, se agregaron al estándar Object.setPrototypeOf
y Object.getPrototypeOf
, para realizar la misma funcionalidad que __proto__
. Como __proto__
se implementó de facto en todas partes, quedó algo obsoleto y llegó al Anexo B del estándar, es decir: opcional para entornos sin navegador.
Más tarde, en el año 2022, se permitió oficialmente usar __proto__
en objetos literales {...}
(eliminado del Anexo B), pero no como getter/setter obj.__proto__
(aún en el Anexo B).
¿Por qué se reemplazó __proto__
por las funciones getPrototypeOf/setPrototypeOf
?
¿Por qué se rehabilitó parcialmente __proto__
y se permitió su uso en {...}
, pero no como captador/definidor?
Esa es una pregunta interesante que requiere que comprendamos por qué __proto__
es malo.
Y pronto obtendremos la respuesta.
No cambies [[Prototype]]
en objetos existentes si la velocidad importa
Técnicamente, podemos obtener/configurar [[Prototype]]
en cualquier momento. Pero normalmente solo lo configuramos una vez en el momento de la creación del objeto y no lo modificamos más: rabbit
hereda del animal
y eso no va a cambiar.
Y los motores 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 interrumpe las optimizaciones internas para las operaciones de acceso a propiedades de objetos. Así que evítalo a menos que sepas lo que estás haciendo o que la velocidad de JavaScript no te importe en absoluto.
Como sabemos, los objetos se pueden utilizar como matrices asociativas 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__"
.
Mira el ejemplo:
dejar objeto = {}; let key = Prompt("¿Cuál es la clave?", "__proto__"); obj[clave] = "algún valor"; alerta(obj[clave]); // [objeto Objeto], ¡no "algún valor"!
Aquí, si el usuario escribe __proto__
, ¡la asignación en la línea 4 se ignora!
Seguramente esto podría resultar sorprendente para alguien que no sea desarrollador, pero bastante comprensible para nosotros. La propiedad __proto__
es especial: debe ser un objeto o null
. Una cadena no puede convertirse en un prototipo. Es por eso que se ignora la asignación de una cadena a __proto__
.
Pero no teníamos intención de implementar ese comportamiento, ¿verdad? Queremos almacenar pares clave/valor y la clave denominada "__proto__"
no se guardó correctamente. ¡Entonces eso es un error!
Aquí las consecuencias no son terribles. Pero en otros casos podemos estar almacenando objetos en lugar de cadenas en obj
, y entonces el prototipo cambiará. Como resultado, la ejecución saldrá mal de maneras totalmente inesperadas.
Lo que es peor: normalmente los desarrolladores no piensan en absoluto en esa posibilidad. Eso hace que estos errores sean difíciles de detectar e incluso convertirlos en vulnerabilidades, especialmente cuando se utiliza JavaScript en el lado del servidor.
También pueden suceder cosas inesperadas al asignar obj.toString
, ya que es un método de objeto integrado.
¿Cómo podemos evitar este problema?
Primero, podemos cambiar y usar Map
para almacenamiento en lugar de objetos simples, entonces todo estará bien:
dejar mapa = nuevo mapa(); let key = Prompt("¿Cuál es la clave?", "__proto__"); map.set(clave, "algún valor"); alerta(map.get(clave)); // "algo de valor" (según lo previsto)
…Pero la sintaxis Object
suele ser más atractiva, ya que es más concisa.
Afortunadamente, podemos utilizar 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__
, se llama al captador/establecedor correspondiente 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 bien, si pretendemos utilizar un objeto como un array asociativo y estar libres de este tipo de problemas, podemos hacerlo con un pequeño truco:
let obj = Object.create(nulo); // o: obj = { __proto__: nulo } let key = Prompt("¿Cuál es la clave?", "__proto__"); obj[clave] = "algún valor"; alerta(obj[clave]); // "algún valor"
Object.create(null)
crea un objeto vacío sin un prototipo ( [[Prototype]]
es null
):
Por lo tanto, no existe un captador/definidor 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 “diccionarios puros”, porque son incluso más simples que el objeto simple normal {...}
.
Una desventaja es que dichos objetos carecen de métodos de objeto integrados, por ejemplo, toString
:
let obj = Object.create(nulo); alerta(obj); // Error (no toString)
…Pero eso suele estar bien para matrices asociativas.
Tenga en cuenta que la mayoría de los métodos relacionados con objetos son Object.something(...)
, como Object.keys(obj)
; no están en el prototipo, por lo que seguirán trabajando en dichos objetos:
let ChineseDictionary = Object.create(null); diccionariochino.hola = "你好"; ChineseDictionary.bye = "再见"; alerta(Object.keys(diccionario chino)); // hola, adios
Para crear un objeto con el prototipo dado, use:
Object.create
proporciona una forma sencilla de realizar una copia superficial de un objeto con todos los descriptores:
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
sintaxis literal: { __proto__: ... }
, permite especificar múltiples propiedades
o Object.create(proto[, descriptores]), permite especificar descriptores de propiedades.
Los métodos modernos para obtener/configurar el prototipo son:
Object.getPrototypeOf(obj): devuelve el [[Prototype]]
de obj
(igual que __proto__
getter).
Object.setPrototypeOf(obj, proto): establece el [[Prototype]]
de obj
en proto
(igual que el definidor __proto__
).
No se recomienda obtener/configurar el prototipo utilizando el __proto__
getter/setter integrado, ahora se encuentra en el Anexo B de la especificación.
También cubrimos objetos sin prototipos, creados con Object.create(null)
o {__proto__: null}
.
Estos objetos se utilizan como diccionarios para almacenar cualquier clave (posiblemente generada por el usuario).
Normalmente, los objetos heredan métodos integrados y __proto__
getter/setter de Object.prototype
, lo que hace que las claves correspondientes estén “ocupadas” y potencialmente causen efectos secundarios. Con un prototipo null
, los objetos están realmente vacíos.
importancia: 5
Hay un dictionary
de objetos, creado como Object.create(null)
, para almacenar cualquier par key/value
.
Agregue el método dictionary.toString()
, que debería devolver una lista de claves delimitadas por comas. Su toString
no debería aparecer en for..in
sobre el objeto.
Así es como debería funcionar:
let diccionario = Object.create(nulo); // tu código para agregar el método Dictionary.toString // agrega algunos datos diccionario.apple = "Apple"; diccionario.__proto__ = "prueba"; // __proto__ es una clave de propiedad normal aquí // sólo apple y __proto__ están en el bucle for(dejar clave en el diccionario) { alerta(clave); // "manzana", luego "__proto__" } // tu toString en acción alerta(diccionario); // "manzana,__proto__"
El método puede tomar todas las claves enumerables usando Object.keys
y generar su lista.
Para 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 propiedades como segundo argumento.
dejar diccionario = Objeto.create(nulo, { toString: { // define la propiedad toString valor() { // el valor es una función return Object.keys(this).join(); } } }); diccionario.apple = "Apple"; diccionario.__proto__ = "prueba"; // apple y __proto__ están en el bucle for(dejar clave en el diccionario) { alerta(clave); // "manzana", luego "__proto__" } // lista de propiedades separadas por comas por toString alerta(diccionario); // "manzana,__proto__"
Cuando creamos una propiedad usando un descriptor, sus indicadores son false
de forma predeterminada. Entonces, en el código anterior, dictionary.toString
no se puede enumerar.
Consulte el capítulo Indicadores y descriptores de propiedades para su revisión.
importancia: 5
Creemos un nuevo objeto rabbit
:
función Conejo(nombre) { this.nombre = nombre; } Rabbit.prototype.sayHola = función() { alerta(este.nombre); }; let conejo = new Conejo("Conejo");
¿Estas llamadas hacen lo mismo o no?
conejo.sayHola(); Conejo.prototipo.sayHola(); Object.getPrototypeOf(conejo).sayHi(); conejo.__proto__.sayHola();
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 demás muestran undefined
:
función Conejo(nombre) { this.nombre = nombre; } Rabbit.prototype.sayHola = función() { alerta (este.nombre); } let conejo = new Conejo("Conejo"); conejo.sayHola(); // Conejo Conejo.prototipo.sayHola(); // indefinido Object.getPrototypeOf(conejo).sayHi(); // indefinido conejo.__proto__.sayHola(); // indefinido