Pregunta 1Si draft es una instancia de una clase sin ninguno de los tres métodos, ¿qué pasa al ejecutar const [firstLine] = draft;?
Métodos especiales — toString, toJSON y Symbol.iterator
Verás toString, toJSON y Symbol.iterator, que JavaScript llama por ti: cómo se ve un objeto como texto, qué va al JSON y cómo recorrer una clase con for...of.
Un objeto de pedido aparece como [object Object] en un template literal, pierde el total que calcula su getter al pasar por JSON.stringify y lanza un TypeError en un bucle for...of.
En este artículo verás tres métodos especiales (métodos que JavaScript busca por su nombre y llama en situaciones concretas): los que se usan al mostrar un valor, al convertirlo a JSON y al recorrerlo.
Cómo se ve un objeto dentro de un texto — toString
Supón que incluyes un pedido en una notificación escribiendo Recibido: ${order}. En lugar del número de pedido o del importe, ves [object Object]. Si en cada pantalla armas el texto con order.id y order.total, el mismo código para construir la cadena termina repetido por todas partes.
toString es un método que JavaScript llama cuando necesita convertir un valor en cadena, y lo que devuelve es la cadena que se usa. Lo llaman ${}, String() y el join de los arrays. Si tu clase no lo define, el toString por defecto de Object.prototype, más arriba en la cadena, devuelve [object Object].
// Una clase sin toString
class DraftOrder {
constructor(id, total) { this.id = id; this.total = total; }
}
// Una clase con toString
class Order {
constructor(id, total) { this.id = id; this.total = total; }
toString() { return `Pedido ${this.id} (${this.total} yenes)`; }
}
const draft = new DraftOrder("A-101", 3200);
const order = new Order("A-102", 4800);
// Al ponerlo en un template literal, se llama a toString
console.log(`Recibido: ${draft}`); // Recibido: [object Object]
console.log(`Recibido: ${order}`); // Recibido: Pedido A-102 (4800 yenes)
// String() y el join de un array usan el mismo toString
console.log(String(order)); // Pedido A-102 (4800 yenes)
console.log([order, new Order("A-103", 1500)].join(" / ")); // Pedido A-102 (4800 yenes) / Pedido A-103 (1500 yenes)
El código que llama, ${}, es el mismo en las dos filas. Para cambiar el formato basta con editar el toString de la clase en un solo lugar, y todas las líneas que insertan el objeto reflejan el cambio. Si pasas la instancia directamente, como en console.log(order), no se llama a toString.
Incluir el total en el JSON que guardas — toJSON
Para enviar un pedido a un servidor, lo conviertes con JSON.stringify(order). En una clase que guarda las líneas del pedido en #lines y calcula el total con un getter total, ninguno de los dos es una propiedad normal de la instancia, así que lo único que se envía es {"id":"A-102"}.
Si escribes toJSON (un método que JSON.stringify llama antes de convertir un valor, y cuyo valor devuelto convierte en su lugar), puedes devolver un objeto con exactamente los valores que quieres enviar. Un Date se convierte en una cadena de fecha y hora en el JSON justamente porque Date tiene este método.
class Order {
#lines; // Líneas del pedido (no se leen desde afuera)
constructor(id, lines) { this.id = id; this.#lines = lines; }
get total() { return this.#lines.reduce((sum, line) => sum + line.price * line.quantity, 0); }
// Lo llama JSON.stringify; lo que se convierte es su valor devuelto
toJSON() { return { id: this.id, itemCount: this.#lines.length, total: this.total }; }
}
const order = new Order("A-102", [{ price: 1200, quantity: 2 }, { price: 2400, quantity: 1 }]);
const other = new Order("A-103", [{ price: 1500, quantity: 1 }]);
// La única clave propia de la instancia es id
console.log(Object.keys(order).join(", ")); // id
// Lo que se convierte es el valor devuelto por toJSON
console.log(JSON.stringify(order)); // {"id":"A-102","itemCount":2,"total":4800}
// Dentro de un array, se llama a toJSON para cada elemento
console.log(JSON.stringify([order, other]));
// [{"id":"A-102","itemCount":2,"total":4800},{"id":"A-103","itemCount":1,"total":1500}]
#lines sigue sin poder leerse desde afuera: es la propia clase la que elige qué valores van al JSON. Incluso un valor que la instancia no guarda como propiedad, como itemCount, puede aparecer en el JSON si lo agregas al objeto que devuelves. Si vuelves a convertir la cadena con JSON.parse, obtienes un objeto plano con esta forma, no una instancia de Order.
Si toJSON devuelve una cadena, se convierte dos veces
Si devuelves una cadena, como en return JSON.stringify({ id: this.id, total: this.total });, esa cadena se vuelve a convertir a JSON. Aun después de un JSON.parse, sigue siendo una cadena, y .total da undefined. Devuelve un objeto.
Recorrer las líneas del pedido con for...of — Symbol.iterator
Ahora quieres mostrar en pantalla las líneas de un pedido, una por fila. Las líneas están ocultas en #lines, así que no se pueden leer desde afuera, y escribir for (const line of order) lanza TypeError: order is not iterable. Aunque guardaras las líneas en un array público, igual no podrías recorrer la instancia en sí.
Si defines un método cuyo nombre es Symbol.iterator (un valor integrado y único que se usa como nombre del método al que llama for...of), for...of obtiene los valores de uno en uno llamando a next() sobre lo que devuelve ese método.
En una clase, un método generador function* nombre() se escribe como *nombre(). Si escribes el nombre entre corchetes, como [Symbol.iterator], el valor que va dentro de los corchetes se usa como nombre del método.
class Order {
#lines; // Líneas del pedido (no se leen desde afuera)
constructor(id, lines) { this.id = id; this.#lines = lines; }
// El método que llama for...of. Cada línea entregada con yield va a la variable del bucle
*[Symbol.iterator]() {
for (const line of this.#lines) yield line;
}
}
const order = new Order("A-102", [{ name: "Café de filtro", quantity: 2 }, { name: "Taza", quantity: 1 }]);
// Obtén las líneas del pedido de una en una desde fuera de la clase
for (const line of order) {
console.log(`${line.name} × ${line.quantity}`); // 2 líneas: Café de filtro × 2 / Taza × 1
}
// Cada spread vuelve a llamar al método, así que el segundo también obtiene 2 elementos
console.log([...order].length, [...order].length); // 2 2
Un generador que ya se leyó hasta el final queda vacío, como viste en el artículo de iteradores y generadores. Quien llama solo recibe las líneas entregadas con yield, nunca el array #lines en sí, así que no puede agregar ni quitar líneas desde afuera.
Devolver el propio array rompe for...of
Si devuelves el array, como en return this.#lines;, los arrays no tienen next(), así que for...of lanza TypeError: undefined is not a function. Un array es iterable, pero no es un iterador. Agrega * y entrega cada elemento con yield.
Encontrar el método que falta a partir del resultado — los tres de un vistazo
Cuando lo que se muestra o el JSON no sale como esperabas, líneas como ${order} y JSON.stringify(order) no mencionan el nombre del método que llaman. Para averiguar qué método olvidaste escribir, razona hacia atrás a partir de cómo se usa el objeto y de la forma del resultado.
Incluso en una instancia de una clase que no tiene ninguno de los tres métodos, cada forma de llamada busca su propio nombre fijo a lo largo de la cadena de prototipos. El único que encuentra es el toString por defecto de Object.prototype; toJSON y [Symbol.iterator] no aparecen aunque se busque hasta llegar a null.
// Una clase sin toString, toJSON ni [Symbol.iterator]
class DraftOrder {
#lines;
constructor(id, lines) { this.id = id; this.#lines = lines; }
get total() { return this.#lines.reduce((sum, line) => sum + line.price * line.quantity, 0); }
}
const draft = new DraftOrder("A-101", [{ name: "Café de filtro", price: 1200, quantity: 2 }]);
// toString se encuentra en Object.prototype; como no hay toJSON, solo se listan sus claves propias
console.log(`${draft}`); // [object Object]
console.log(JSON.stringify(draft)); // {"id":"A-101"}
// [Symbol.iterator] no existe en ningún lado, así que se lanza un error
try {
console.log([...draft]);
} catch (error) {
console.log(error.message); // draft is not iterable
}
JSON.stringify no lanza ningún error: devuelve {"id":"A-101"}, así que los datos pueden enviarse sin las líneas de #lines ni el total, y nadie lo nota hasta que llegan al otro lado. La tabla de abajo reúne otras formas que buscan los mismos nombres y qué pasa cuando el método no está escrito.
| Forma de llamada | Método que busca | Resultado sin el método |
|---|---|---|
| JSON.stringify([draft]) | toJSON (por elemento) | [{"id":"A-101"}] |
| for (const line of draft) | [Symbol.iterator] | TypeError: draft is not iterable |
| const [firstLine] = draft | [Symbol.iterator] | TypeError: draft is not iterable |
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2¿Qué pasa al aplicar JSON.stringify a un array de dos pedidos cuyo toJSON devuelve { id: this.id }?
Pregunta 3Si escribes [...cart] dos veces con un cart que tiene *[Symbol.iterator](), ¿qué longitud tiene el segundo array?