Ir al contenido
wannacode

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.

Seat
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)));
Salida
undefined
false
María está en la fila 5, asiento 12, pero get 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

JavaScript no tiene 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}`);
Salida
María
true
Seat(5, 12)
Fila 5, asiento 12
  • Object.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 un Map, y get ahora 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
  1. Un Map vacío: la clave es el asiento del cine como cadena, el valor es un nombre.
  2. set añade la clave "5-12", hecha con Seat(5, 12), con el valor «María».
  3. get recibe la clave de un objeto Seat nuevo. El objeto es otro, no el de set.
  4. key() hace la cadena con los campos, así que es la misma: "5-12".
  5. Las dos cadenas se hacen con campos iguales: fila 5 y 5, asiento 12 y 12. Son iguales, y get devuelve «María».
  6. has hace lo mismo con la clave de otro Seat nuevo más: true.
  7. 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.row se 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);
Salida
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 con import:
"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}`);
}
Salida
TypeError: Cannot assign to read only property 'row' of object '[object Object]'

La idea

Comprobaciones y métodos

Un precio no puede ser negativo. La comprobación está al principio del constructor, antes de que se asignen los campos.
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);
}
Salida
Price(18.5 EUR)
Price(14.8 EUR)
El precio no puede ser negativo: -5
  • RangeError es el tipo de error incorporado para un valor fuera del rango permitido. Si el constructor lanza una excepción, el objeto no se crea.
  • withDiscount no cambia el precio. Devuelve un Price nuevo. Así se trabaja con objetos que no cambian: coffee sigue 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);
Respuesta

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.

La idea

switch con return

JavaScript no tiene una expresión 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} €`);
Salida
Envío: 4.5 €
  • return sale de la función, así que después de él no hay caída, y no escribes break.
  • Dos casos con el mismo código se escriben uno debajo del otro: case "mensajero": está vacío y cae a case "domicilio":. Esa es la caída útil.
  • default atrapa 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 devuelve undefined, sin error:
function deliveryPrice(method) {
switch (method) {
case "oficina":
return 4.5;
case "mensajero":
return 6.9;
}
}
console.log(deliveryPrice("correo"));
Salida
undefined

Si 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));
Salida
6.9
9.9
Má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");
Salida
6.9
método desconocido

La 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

La comisión depende de la forma de pago. JavaScript no tiene patrones de tipo como los de un 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));
Salida
3
0.5
2

El 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}`);
}
Salida
TypeError: Pago desconocido: Voucher

Los 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()));
Salida
tarjeta ****4444
transferencia desde una cuenta española
transferencia desde el extranjero
contra reembolso
Má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));
Salida
ES
ES

Pruébalo

¿Qué caso gana?

Las comprobaciones van de arriba abajo. Gana la primera que es verdadera.

¿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));
Respuesta
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));
Salida
contra reembolso

La 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 esquina
Café 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);
Salida
La tienda de la esquina
Café 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, y toFixed(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} €`);
}
Salida
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.
Salida
círculo: 3.14
rectángulo: 6.00
cuadrado: 16.00
triángulo: 7.50
Total: 32.64
2. Precio del envío. Tres clases: ToOffice 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.
Salida
ToOffice(Valencia Centro) -> 4.50 €
ToAddress(Madrid, 2) -> 5.90 €
ToAddress(Valencia, 8) -> 9.90 €
Pickup() -> 0.00 €
3. Un punto como clave. El mensajero pasa por puntos del mapa: (2, 3), (5, 1), (2, 3), (0, 0), (5, 1), (2, 3). Haz una clase 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).
Salida
(2, 3): 3
(5, 1): 2
(0, 0): 1
Visitas a (2, 3): 3

Más tareas

  • Un estudiante como clase de datos. Student con name y grade, cuyo constructor lanza un RangeError para una nota fuera del rango de 2 a 6 y congela el objeto. Ordena un array de estudiantes con toSorted((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 switch y return: los casos del 1 al 5 uno debajo del otro, y luego return "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

1. const m = new Map(); m.set(new Seat(5, 12), "María"); ¿Qué da m.get(new Seat(5, 12))?
2. ¿Qué hace Object.freeze(this) al final del constructor?
3. ¿Qué pasa en un switch si un caso no tiene ni break ni return?
4. ¿Qué hace const { iban } = payment;?
5. JavaScript no tiene sealed. ¿Cómo se protege de un tipo desconocido la función que distingue los tipos de pago?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·