Datos, switch y desestructuración
En resumen: JavaScript no tiene record: una clase de datos se hace con Object.freeze, y switch con return, instanceof y la desestructuración sustituyen a los patrones.
Problema
El asiento está ocupado, pero Map no lo encuentra
Reservas en un cine: la clave es el asiento (fila y número), el valor es el nombre de la persona.
class Seat { constructor(row, number) { this.row = row; this.number = number; }}const reservations = new Map();reservations.set(new Seat(5, 12), "María");
console.log(reservations.get(new Seat(5, 12)));console.log(reservations.has(new Seat(5, 12)));undefinedfalseget devuelve undefined. new Seat(5, 12) escrito dos veces son dos objetos distintos, y un Map compara las claves que son objetos según si son el mismo objeto. JavaScript no tiene métodos, como equals y hashCode en Java, con los que una clase diga cuándo dos objetos son iguales.La idea
Una clase para datos y una clave hecha con los campos
record. Lo que Java escribe solo, aquí lo escribes tú:class Seat { constructor(row, number) { this.row = row; this.number = number; Object.freeze(this); }
key() { return `${this.row}-${this.number}`; }
toString() { return `Seat(${this.row}, ${this.number})`; }}const reservations = new Map();reservations.set(new Seat(5, 12).key(), "María");
console.log(reservations.get(new Seat(5, 12).key()));console.log(reservations.has(new Seat(5, 12).key()));
const seat = new Seat(5, 12);console.log(`${seat}`);console.log(`Fila ${seat.row}, asiento ${seat.number}`);MaríatrueSeat(5, 12)Fila 5, asiento 12Object.freeze(this)al final del constructor prohíbe cambiar los campos.key()hace una cadena con los campos:"5-12". Dos cadenas con los mismos caracteres son iguales, así que la cadena es una buena clave para unMap, ygetahora encuentra a María.toString()dice cómo se ve el objeto como texto.
Los campos se leen directamente: seat.row.
const reservations = new Map();reservations.set(new Seat(5, 12).key(), "María");
console.log(reservations.get(new Seat(5, 12).key()));console.log(reservations.has(new Seat(5, 12).key()));
const seat = new Seat(5, 12);console.log(`${seat}`);console.log(`Fila ${seat.row}, asiento ${seat.number}`);Paso a paso, en palabras
- Un Map vacío: la clave es el asiento del cine como cadena, el valor es un nombre.
- set añade la clave "5-12", hecha con Seat(5, 12), con el valor «María».
- get recibe la clave de un objeto Seat nuevo. El objeto es otro, no el de set.
- key() hace la cadena con los campos, así que es la misma: "5-12".
- Las dos cadenas se hacen con campos iguales: fila 5 y 5, asiento 12 y 12. Son iguales, y get devuelve «María».
- has hace lo mismo con la clave de otro Seat nuevo más: true.
- Aquí toString se escribe a mano, y los campos se leen directamente: seat.row.
Más precisamente, para curiosos
- Cambiar un campo congelado no se aplica:
seat.rowse queda en 5. En un archivo normal no hay error, y el descuido es difícil de detectar. La inmutabilidad también es solo de un nivel: un array en un campo puede cambiar.
class Seat { constructor(row, number) { this.row = row; this.number = number; Object.freeze(this); }
key() { return `${this.row}-${this.number}`; }
toString() { return `Seat(${this.row}, ${this.number})`; }}
const seat = new Seat(5, 12);seat.row = 6;console.log(seat.row);
class Cart { constructor(items) { this.items = items; Object.freeze(this); }}const cart = new Cart(["café"]);cart.items.push("miel");console.log(cart.items);5[ 'café', 'miel' ]- En modo estricto el mismo cambio se detiene con un
TypeError. El modo estricto abarca el código dentro de una clase, un archivo que empieza con"use strict";y un módulo conimport:
"use strict";
class Seat { constructor(row, number) { this.row = row; this.number = number; Object.freeze(this); }
key() { return `${this.row}-${this.number}`; }
toString() { return `Seat(${this.row}, ${this.number})`; }}
const seat = new Seat(5, 12);try { seat.row = 6;} catch (e) { console.log(`${e.name}: ${e.message}`);}TypeError: Cannot assign to read only property 'row' of object '[object Object]'La idea
Comprobaciones y métodos
class Price { constructor(amount, currency) { if (amount < 0) { throw new RangeError(`El precio no puede ser negativo: ${amount}`); } this.amount = amount; this.currency = currency; Object.freeze(this); }
withDiscount(percent) { return new Price(this.amount * (1 - percent / 100), this.currency); }
toString() { return `Price(${this.amount} ${this.currency})`; }}const coffee = new Price(18.5, "EUR");const sale = coffee.withDiscount(20);console.log(`${coffee}`);console.log(`${sale}`);
try { new Price(-5, "EUR");} catch (e) { console.log(e.message);}Price(18.5 EUR)Price(14.8 EUR)El precio no puede ser negativo: -5RangeErrores el tipo de error incorporado para un valor fuera del rango permitido. Si el constructor lanza una excepción, el objeto no se crea.withDiscountno cambia el precio. Devuelve unPricenuevo. Así se trabaja con objetos que no cambian:coffeesigue en 18,50 €.
Pruébalo
¿Son iguales?
Seat es la clase con key() de la tarjeta anterior.¿Qué imprimirá el programa?
const a = new Seat(5, 12);const b = new Seat(5, 12);console.log(a === b);console.log(a.key() === b.key());console.log(new Set([a, b]).size);a y b son dos objetos, así que === es false: pregunta si son el mismo objeto. Las claves son cadenas hechas con los mismos campos, y son iguales. Un Set, como un Map, compara los objetos según si son el mismo objeto, así que entran los dos: el tamaño es 2.Pruébalo
El envío sale caro
El envío a una oficina cuesta 4,50 €, pero el programa dice 6,90 €. ¿En qué línea está el error?
Elige la línea con el error.
price = 4.5; no hay break. El switch de JavaScript no se detiene al final del caso, sino que cae al siguiente: también ejecuta price = 6.9;, y solo entonces llega al break. La solución es un break después de la línea 5. JavaScript no tiene un switch que devuelva un valor, pero return también termina el caso. Eso es la siguiente tarjeta.La idea
switch con return
switch que dé un valor. Así que el switch va dentro de una función, y cada caso termina con return.function deliveryPrice(method) { switch (method) { case "oficina": return 4.5; case "mensajero": case "domicilio": return 6.9; case "tienda": return 0; default: throw new Error(`Método de envío desconocido: ${method}`); }}
const price = deliveryPrice("oficina");console.log(`Envío: ${price} €`);Envío: 4.5 €returnsale de la función, así que después de él no hay caída, y no escribesbreak.- Dos casos con el mismo código se escriben uno debajo del otro:
case "mensajero":está vacío y cae acase "domicilio":. Esa es la caída útil. defaultatrapa el resto. Aquí lanza una excepción.- JavaScript no comprueba que cada entrada esté cubierta. Sin
default, ningún caso encaja con una forma desconocida: la función llega a su final y devuelveundefined, sin error:
function deliveryPrice(method) { switch (method) { case "oficina": return 4.5; case "mensajero": return 6.9; }}
console.log(deliveryPrice("correo"));undefinedSi un caso tiene varias líneas y su propio const, va entre { }, para que no se mezclen con los otros casos:
function delivery(method, weightKg) { switch (method) { case "oficina": return 4.5; case "mensajero": { const base = 6.9; return weightKg > 10 ? base + 3 : base; } default: return 0; }}
console.log(delivery("mensajero", 4));console.log(delivery("mensajero", 12));6.99.9Más precisamente, para curiosos
Cuando cada caso es solo un valor, puedes usar un objeto como tabla de búsqueda en lugar de un switch. ?? da la alternativa para una clave que no está:
const PRICES = { "oficina": 4.5, "mensajero": 6.9, "tienda": 0 };console.log(PRICES["mensajero"]);console.log(PRICES["correo"] ?? "método desconocido");6.9método desconocidoLa idea
Una familia de tipos
La tienda acepta tres formas de pago: tarjeta, transferencia bancaria y contra reembolso. No hay otras. Cada forma es su propia clase:
class CardPayment { constructor(cardNumber) { this.cardNumber = cardNumber; }}
class BankTransfer { constructor(iban) { this.iban = iban; }}
class CashOnDelivery {}Cada forma de pago son solo datos: un número de tarjeta, un IBAN o nada. CashOnDelivery no tiene campos: su cuerpo está vacío.
En Java, sealed hace que el compilador rechace un cuarto tipo. JavaScript no tiene forma de decir «solo estos tres». Una cuarta clase puede aparecer en cualquier parte. Así que la función que distingue los tipos tiene que protegerse de un tipo desconocido. Eso es la siguiente tarjeta.
Míralo
instanceof y desestructuración
switch de Java. La comprobación se hace con instanceof, una tras otra:function fee(payment, amount) { if (payment instanceof CardPayment) { return amount * 0.015; } if (payment instanceof BankTransfer) { return 0.5; } if (payment instanceof CashOnDelivery) { return 2; } throw new TypeError(`Pago desconocido: ${payment.constructor.name}`);}console.log(fee(new CardPayment("4111222233334444"), 200));console.log(fee(new BankTransfer("ES9121000418450200051332"), 200));console.log(fee(new CashOnDelivery(), 200));30.52El throw del final es la protección: si el objeto no es ninguno de los tres tipos, el programa se detiene con un mensaje claro en lugar de devolver undefined en silencio:
class Voucher {}
try { fee(new Voucher(), 200);} catch (e) { console.log(`${e.name}: ${e.message}`);}TypeError: Pago desconocido: VoucherLos campos se sacan con desestructuración: const { iban } = payment; toma el campo iban del objeto y lo pone en una variable con el mismo nombre. La condición para una cuenta española es un ? : normal.
function describe(payment) { if (payment instanceof CardPayment) { const { cardNumber } = payment; return `tarjeta ****${cardNumber.slice(-4)}`; } if (payment instanceof BankTransfer) { const { iban } = payment; return iban.startsWith("ES") ? "transferencia desde una cuenta española" : "transferencia desde el extranjero"; } if (payment instanceof CashOnDelivery) { return "contra reembolso"; } throw new TypeError(`Pago desconocido: ${payment.constructor.name}`);}console.log(describe(new CardPayment("4111222233334444")));console.log(describe(new BankTransfer("ES9121000418450200051332")));console.log(describe(new BankTransfer("DE89370400440532013000")));console.log(describe(new CashOnDelivery()));tarjeta ****4444transferencia desde una cuenta españolatransferencia desde el extranjerocontra reembolsoMás precisamente, para curiosos
- Al desestructurar, la variable puede tener otro nombre:
const { iban: account } = transfer;. También puedes hacerlo directamente en los parámetros de una función:({ iban }) => ....
const transfer = new BankTransfer("ES9121000418450200051332");const { iban: account } = transfer;console.log(account.slice(0, 2));
const country = ({ iban }) => iban.slice(0, 2);console.log(country(transfer));ESESPruébalo
¿Qué caso gana?
¿Qué imprimirá el programa?
function label(payment, amount) { if (payment instanceof CashOnDelivery && amount > 500) { return "contra reembolso: no se acepta por encima de 500 €"; } if (payment instanceof CashOnDelivery) { return "contra reembolso"; } if (payment instanceof CardPayment && amount > 1000) { return "tarjeta: hace falta confirmación"; } if (payment instanceof CardPayment) { return "tarjeta"; } return "transferencia";}
console.log(label(new CashOnDelivery(), 650));console.log(label(new CashOnDelivery(), 80));console.log(label(new CardPayment("4111222233334444"), 650));amount > 500 y llega a la segunda. La tarjeta es por 650 €, y la confirmación hace falta por encima de 1000 €, así que gana la comprobación sin condición sobre el importe: «tarjeta».Más precisamente, para curiosos
La comprobación con condición tiene que ir antes de la general para el mismo tipo. Al revés no es un error: la comprobación general lo atrapa todo, y a la de la condición simplemente nunca se llega:
function label(payment, amount) { if (payment instanceof CashOnDelivery) { return "contra reembolso"; } if (payment instanceof CashOnDelivery && amount > 500) { return "contra reembolso: no se acepta por encima de 500 €"; } return "otro";}
console.log(label(new CashOnDelivery(), 650));contra reembolsoLa idea
Texto en varias líneas
Un texto de varias líneas, como un recibo o un JSON, se lee mal con " y \n en cada línea. Una plantilla literal (template literal) entre dos comillas invertidas (`) puede ocupar varias líneas:
const receipt = `La tienda de la esquinaCafé en grano 18.50 €Miel 12.00 €Total: 30.50 €`;console.log(receipt);
const name = "María";const total = 75.5;const json = `{"customer": "${name}", "total": ${total.toFixed(2)}}`;console.log(json);La tienda de la esquinaCafé en grano 18.50 €Miel 12.00 €Total: 30.50 €{"customer": "María", "total": 75.50}- El texto empieza justo después de la comilla invertida de apertura. Por eso la primera línea está en la misma línea.
- La sangría se conserva tal como está escrita. Por eso las líneas aquí están pegadas a la izquierda.
- Las comillas normales de dentro no se escapan.
${...}rellena los valores, ytoFixed(2)da dos cifras después del punto decimal.
En Java, var deja que el compilador deduzca el tipo de la variable. En JavaScript, var es la forma antigua de crear una variable y no se usa en este curso. const y let tampoco escriben un tipo: el tipo pertenece al valor, no al nombre.
const prices = new Map([ ["café", 18.5], ["miel", 12],]);for (const [name, price] of prices) { console.log(`${name}: ${price} €`);}café: 18.5 €miel: 12 €Ejercicio
Datos y casos
Resuelve las tareas en tu editor.
1. Figuras. Tres clases:Circle con radius, Rectangle con width y height, y Triangle con base y height. Escribe area(shape) con instanceof y desestructuración, y lanza un TypeError para un tipo desconocido. Escribe también name(shape), que dice «cuadrado» para un rectángulo con los lados iguales. Imprime un círculo de radio 1, un rectángulo de 2 por 3, un cuadrado de 4 por 4, un triángulo de base 3 y altura 5, y el área total.círculo: 3.14rectángulo: 6.00cuadrado: 16.00triángulo: 7.50Total: 32.64ToOffice con office, ToAddress con city y weightKg, y Pickup sin campos, cada una con su propio toString(). El precio: a una oficina 4,50 €; a domicilio 5,90 € en Madrid y 6,90 € en otros lugares, más 1 € por cada kilogramo por encima de 5; recogida en la tienda 0 €. Imprime cada envío (con su toString()) y su precio.ToOffice(Valencia Centro) -> 4.50 €ToAddress(Madrid, 2) -> 5.90 €ToAddress(Valencia, 8) -> 9.90 €Pickup() -> 0.00 €Point con x, y y key(), que devuelve la cadena (2, 3), y cuenta cuántas veces estuvo en cada punto en un Map con (visits.get(key) ?? 0) + 1. Al final, busca la clave de new Point(2, 3).(2, 3): 3(5, 1): 2(0, 0): 1Visitas a (2, 3): 3Más tareas
- Un estudiante como clase de datos.
Studentconnameygrade, cuyo constructor lanza unRangeErrorpara una nota fuera del rango de 2 a 6 y congela el objeto. Ordena un array de estudiantes contoSorted((a, b) => a.grade - b.grade). - Día de la semana. Reescribe la tarea «día de la semana» de la lección «Condiciones» como una función con
switchyreturn: los casos del 1 al 5 uno debajo del otro, y luegoreturn "laborable";. - Una carta. Con una plantilla literal de varias líneas, haz una carta que confirme una reserva: nombre, fecha, número de invitados y precio en euros.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Map y Set · Códigos hash y funciones hash · Funciones flecha · Métodos de arrays
- Continúa en: Patrones de diseño · Bases de datos · Observer · Factory Method y Simple Factory
- Ejercicios sobre el tema: JavaScript moderno