Clases anidadas y datos
En resumen: Una clase anidada es una clase dentro de otra porque solo tiene sentido ahí; en JavaScript es un campo estático, y los datos se anidan como objetos y arrays unos dentro de otros.
Problema
Una clase que solo tiene sentido en un lugar
En la lección sobre la lista enlazada, Node era una clase aparte. Cualquiera en el programa puede crear nodos, pero solo tienen sentido en la lista. Lo mismo pasa con una línea de pedido:
class OrderLine { constructor(product, quantity, price) { this.product = product; this.quantity = quantity; this.price = price; }}
class Order { lines = [];}OrderLine sin Order no tiene sentido. El nombre es largo para que no choque con la línea de una factura, InvoiceLine, ni con la línea de un almacén. Y nada dice que las dos clases van juntas. ¿Puede la línea vivir dentro del pedido?
La idea
Clase anidada
Puede. JavaScript no tiene declaración de una clase dentro de otra, pero una clase es un valor y puede ser un campo estático. Así, desde fuera se llama Order.Line, como una clase anidada en Java:
class Order { static Line = class { constructor(product, quantity, price) { this.product = product; this.quantity = quantity; this.price = price; }
total() { return this.quantity * this.price; } };
#lines = [];
add(line) { this.#lines.push(line); }
total() { let sum = 0; for (const line of this.#lines) { sum += line.total(); } return sum; }}const order = new Order();order.add(new Order.Line("Café en grano", 2, 12.5));order.add(new Order.Line("Taza", 4, 3.0));console.log(`Total: ${order.total().toFixed(2)} €`);Total: 37.00 €static Line = class { ... }es un campo estático deOrdercuyo valor es una clase sin nombre. Desde fuera y dentro de los métodos, esOrder.Line.new Order.Line(...)se crea sin un objetoOrder, como una clase normal.Order.Lineno está ligada a un pedido concreto y no ve sus campos. En Java una clase así se llama estática.
Esto se ve con menos frecuencia. A menudo la clase pequeña simplemente está al lado de la grande, en el mismo archivo, o en lugar de una clase hay un objeto con campos. De eso trata la siguiente tarjeta.
La idea
Datos dentro de datos: objetos y arrays
Si la línea de pedido solo guarda valores y no tiene métodos, en JavaScript a menudo no hay una clase para ella. El pedido es un objeto que guarda un array de objetos:
const order = { customer: "Fernández", lines: [ { product: "Café en grano", quantity: 2, price: 12.5 }, { product: "Taza", quantity: 4, price: 3.0 }, ],};
let total = 0;for (const line of order.lines) { total += line.quantity * line.price;}console.log(order.lines[0].product);console.log(`Total: ${total.toFixed(2)} €`);Café en granoTotal: 37.00 €order.lineses el array,order.lines[0]es la primera línea, yorder.lines[0].productes el producto que hay en ella. Cada punto o[...]baja un nivel más.- Aquí el objeto es para campos conocidos (
customer,lines). Para claves que llegan mientras el programa se ejecuta estáMap, como en la lección sobre mapas. - Una clase es mejor opción cuando los datos tienen comportamiento (un método
total) o reglas que hay que cumplir.
Un Map de arrays reúne cosas en grupos:
const people = [["Juan", "Sevilla"], ["María", "Valencia"], ["Ana", "Sevilla"]];const byCity = new Map();for (const [name, city] of people) { if (!byCity.has(city)) { byCity.set(city, []); } byCity.get(city).push(name);}console.log(byCity);console.log(byCity.get("Sevilla")[1]);Map(2) { 'Sevilla' => [ 'Juan', 'Ana' ], 'Valencia' => [ 'María' ] }AnaMás precisamente, para curiosos
- Una trampa con los arrays anidados:
fillpone una misma referencia en todos los lugares. Las tres filas son un mismo array:
let grid = Array(3).fill([0, 0, 0]);grid[0][0] = 1;console.log(grid);
grid = [];for (let row = 0; row < 3; row++) { grid.push([0, 0, 0]);}grid[0][0] = 1;console.log(grid);[ [ 1, 0, 0 ], [ 1, 0, 0 ], [ 1, 0, 0 ] ][ [ 1, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ]console.logmuestra solo unos pocos niveles. Más abajo escribe[Array]u[Object].JSON.stringifymuestra el objeto completo:
const shop = { name: "Rincón del Café", menu: { drinks: { hot: ["café", "té"] } } };console.log(shop);console.log(JSON.stringify(shop));{ name: 'Rincón del Café', menu: { drinks: { hot: [Array] } } }{"name":"Rincón del Café","menu":{"drinks":{"hot":["café","té"]}}}La idea
El objeto anidado guarda al externo
El carrito tiene un descuento para todos los productos que hay en él. Cada producto tiene que conocer el descuento de su propio carrito. En JavaScript una clase anidada no ve al objeto externo, así que el carrito se pasa de forma explícita:
class Cart { static Item = class { constructor(cart, name, price) { this.cart = cart; this.name = name; this.price = price; }
finalPrice() { return (this.price * (100 - this.cart.#discountPercent)) / 100; } };
#discountPercent; #items = [];
constructor(discountPercent) { this.#discountPercent = discountPercent; }
add(name, price) { this.#items.push(new Cart.Item(this, name, price)); }
print() { for (const item of this.#items) { console.log(`${item.name}: ${item.finalPrice().toFixed(2)} €`); } }}const cart = new Cart(10);cart.add("Auriculares", 80);cart.add("Funda", 20);cart.print();Auriculares: 72.00 €Funda: 18.00 €add pasa this, e Item la guarda en this.cart. finalPrice lee el #discountPercent del carrito en el que está. Item está escrita dentro de Cart, así que ve los campos # del carrito, igual que una clase interna en Java ve los private.const cart = new Cart(10);cart.add("Auriculares", 80);cart.add("Funda", 20);cart.print();Paso a paso, en palabras
- El carrito es un objeto Cart con un descuento del 10 %.
- add crea un Item y le pasa el carrito, this. Además de name y price, cada Item guarda en cart una flecha a su propio carrito.
- El segundo Item apunta al mismo carrito.
- finalPrice de «Auriculares» sigue la flecha this.cart y lee #discountPercent: 80 * (100 - 10) / 100 = 72.
- finalPrice de «Funda» sigue la flecha this.cart y lee #discountPercent: 20 * (100 - 10) / 100 = 18.
Más precisamente, para curiosos
- Desde fuera, un objeto
Itemse crea con el carrito como primer argumento. Normalmente la clase externa crea las internas ella misma, comoaddaquí:
const cart = new Cart(10);const cable = new Cart.Item(cart, "Cable", 10);console.log(cable.finalPrice());9- El campo
cartes justo lo que hace elCart.thisoculto en Java. Aquí simplemente se ve. - Si la clase anidada no usa los campos de la clase externa, no le des el objeto externo. Así no guarda una referencia que no necesita.
Pruébalo
Dos carritos
La clase Cart es la misma. Un carrito no tiene descuento, y el otro tiene un 20 % para los socios del club.
¿Qué imprimirá el programa?
const regular = new Cart(0);const member = new Cart(20);regular.add("Libro", 15);member.add("Libro", 15);regular.print();member.print();Cada Item ve el descuento del carrito que lo creó. El libro de regular tiene un 0 %, y el de member, un 20 %.
Pruébalo
Encuentra el error
console.log(new Order.Line("Café", 12.5).show()) el programa se detiene:ReferenceError: currency is not defined¿En qué línea está el problema?
Elige la línea con el error.
currency es un campo de un objeto Order, y Line no tiene pedido. JavaScript busca un nombre suelto currency como variable y no lo encuentra. El error solo aparece cuando se llama a show, no antes. La corrección es que Line reciba el pedido, new Order.Line(order, "Café", 12.5), y lea this.order.currency. Otra forma es que currency sea un campo estático, Order.currency, si la moneda es la misma para todos los pedidos.La idea
Una clase dentro de una función
Una clase también se puede declarar dentro de una función. Entonces solo se ve ahí y se llama clase local:
function printBestSeller(names, sold) { class Sale { constructor(name, count) { this.name = name; this.count = count; } }
let best = new Sale(names[0], sold[0]); for (let i = 1; i < names.length; i++) { if (sold[i] > best.count) { best = new Sale(names[i], sold[i]); } } console.log(`Más vendido: ${best.name} (${best.count} vendidos)`);}const names = ["Café", "Té", "Agua"];const sold = [120, 45, 300];printBestSeller(names, sold);Más vendido: Agua (300 vendidos)Sale mantiene juntos el nombre y la cantidad. Nadie la usa fuera de printBestSeller, así que vive ahí. Estas clases son raras, pero conviene conocerlas.
La idea
Un objeto en lugar de una clase
Un descuento de la tienda es algo con un método apply(price). En el módulo «Herencia y polimorfismo» viste que JavaScript no tiene interface: al objeto le basta con tener los métodos. Podrías crear una clase para cada descuento: class BlackFriday { apply(price) { ... } }. En Java, para un descuento que se usa en un solo lugar, existe la clase anónima: una clase sin nombre, escrita justo ahí. En JavaScript normalmente ni siquiera necesitas una clase. Escribes directamente el objeto con el método:
const blackFriday = { apply(price) { return (price * 70) / 100; },};const none = { apply(price) { return price; },};console.log(blackFriday.apply(100));console.log(none.apply(100));70100{ apply(price) { ... } }es un objeto con un método. No hay clase, yblackFriday.apply(100)funciona igual que con un objeto de una clase.- JavaScript imprime
70, no70.0: el número es entero.
Más precisamente, para curiosos
- JavaScript sí tiene una clase sin nombre:
class { ... }es una expresión y se puede crear al momento. Rara vez se escribe, porque el objeto con un método es más corto:
const none = new (class { apply(price) { return price; }})();console.log(none.apply(100));console.log(none.constructor.name === "");100true- Cuando el descuento es una sola acción, en JavaScript a menudo pasas directamente una función, como la función de comparación de la siguiente tarjeta. En el módulo «JavaScript moderno» verás una forma más corta de escribir funciones pequeñas.
La idea
Función de comparación: ordenar con tu propia regla
sort ordenaba texto, y ordenaba números con una función. Con alumnos y sin función, no da error, pero ordena según el texto de toString:
class Student { constructor(name, grade) { this.name = name; this.grade = grade; }
toString() { return `${this.name} ${this.grade}`; }}const students = [ new Student("Juan", 5.50), new Student("María", 6.00), new Student("Pedro", 4.75), new Student("Ana", 5.50),];students.sort();console.log(students.join(", "));Ana 5.5, Juan 5.5, María 6, Pedro 4.75Aquí salieron por nombre por casualidad, porque toString empieza con el nombre. Sin toString, cada alumno sería el texto [object Object].
La regla se da como una función de comparación, como con los números en la lección sobre arrays. Recibe dos alumnos a y b y devuelve un número negativo si a debe ir antes que b, cero si son iguales y un número positivo si a va después de b:
students.sort(function (a, b) { return b.grade - a.grade;});console.log(students.join(", "));María 6, Juan 5.5, Ana 5.5, Pedro 4.75b.grade - a.gradecompara al revés, así que la nota más alta va primero.- Juan y Ana tienen la misma nota y se quedan en el orden en que estaban.
sortno intercambia elementos iguales. joinllama atoStringde cada alumno.- Para texto en orden inverso:
sort()y despuésreverse().
const cities = ["Sevilla", "Valencia", "Bilbao"];cities.sort();cities.reverse();console.log(cities);[ 'Valencia', 'Sevilla', 'Bilbao' ]Más precisamente, para curiosos
sortordena el propio array. Si necesitas uno nuevo, estátoSortedcon la misma función.b.grade - a.gradees seguro, porque los números en JavaScript no se desbordan comointen Java. Para el texto no hay resta: ahí se usaa.name.localeCompare(b.name)de la lección sobre texto.
Pruébalo
¿Cuál va primero?
¿Qué imprimirá el programa?
const products = [ new Product("Monitor", 220), new Product("Ratón", 25), new Product("Teclado", 60),];
products.sort(function (a, b) { return a.price - b.price;});for (const p of products) { console.log(p.name);}a.price - b.price es negativo cuando a es más barato. Así que el más barato va primero: 25, 60, 220. Para poner primero el más caro, cambia a por b.Ejercicio
Clases que van juntas
Resuelve las tareas en tu editor.
1. Carrito de compras. Una clase ShoppingCart con una clase anidada Item (nombre y precio, static Item = class { ... }) y los métodos addItem(item), getTotal() y count(). Agrega new ShoppingCart.Item("Leche", 2.5), pan por 1,20 € y café por 7,50 €. Imprime el total con toFixed(2).
Productos: 3Total: 11.20 €2. Monitor de un servidor web. Una clase WebServer con una clase anidada Request (dirección y código de estado) y los métodos addRequest(r) y printStats(). Las estadísticas dicen cuántas solicitudes hay, cuántas hay de cada código de estado (ordenados como números) y qué porcentaje son correctas, es decir, con un código del 200 al 299. Guarda los conteos en un Map. Las solicitudes son /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Solicitudes: 5200: 3401: 1500: 1Correctas: 60%3. Administrador de un edificio. Una clase Building con una clase anidada Apartment (número, número de habitaciones, propietario) y los métodos addApartment(a) y getApartmentInfo(number). Guarda los apartamentos en un Map de número a apartamento. Agrega el 101, de 3 habitaciones, de Fernández, y el 102, de 2 habitaciones, de Pérez. Busca el 101 y el 999.
Apartamento 101: 3 habitaciones, propietario: FernándezNo existe el apartamento 999.4. Clasificación. Ordena a los alumnos de la lección por nota, de la más alta a la más baja. Cuando las notas son iguales, ordénalos por nombre, en orden alfabético. Todo va en una sola función de comparación: si las notas son distintas, devuelve b.grade - a.grade; si no, compara los nombres con localeCompare.
María 6Ana 5.5Juan 5.5Pedro 4.75Más tareas
- Configurador de computadoras. Una clase
Computercon las clases anidadasCPU(marca, velocidad en GHz),RAM(GB) yStorage(GB) y los métodossetCPU,setRAM,setStorage.toStringdescribe la configuración:Intel 3.5 GHz, 16 GB RAM, 512 GB. - El planificador de viajes, otra vez. Haz la tarea de la lección sobre
Mapuna vez con una clase anidadaTripPlanner.Destinationque guarde el nombre y el array de actividades, y otra vez solo con unMapde arrays. ¿Cuál es más práctica? - La red de contactos, otra vez. Mueve
Contactde la tarea de la lección sobreMapdentro deContactNetwork. ¿DeberíaContactconocer su red? - Cálculo mental, otra vez. Mueve
Questionde la tarea de la lección sobre arrays dentro deMathGame. - Productos de dos formas. Ordena un array de productos por precio, del más caro, y por nombre cuando el precio es el mismo. Después ordena el mismo array solo por nombre, en orden inverso.
- Tu propio descuento. Escribe tres objetos con un método
apply: un 10 % a partir de 100 €, 5 € fijos de descuento y «el segundo producto a mitad de precio». Aplícalos a un mismo carrito.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Cadenas · Arrays: push, splice y sort · Pila, cola y lista enlazada · Map y Set
- Continúa en: Funciones flecha · Métodos de arrays · Patrones de diseño · Tests con node:test
- Ejercicios sobre el tema: Colecciones