Класове в класове и данни в данни
Проблем
Клас, който има смисъл само на едно място
В урока за свързания списък Node беше отделен клас. Всеки в програмата може да прави възли, а те имат смисъл само в списъка. Същото е с реда от поръчката:
class OrderLine { constructor(product, quantity, price) { this.product = product; this.quantity = quantity; this.price = price; }}
class Order { lines = [];}OrderLine без Order е безсмислен. Името е дълго, за да не се сблъска с реда от фактурата, InvoiceLine, или с реда от склада. А и нищо не казва, че двата класа вървят заедно. Може ли редът да живее вътре в поръчката?
Идеята
Вложен клас
Може. Декларация на клас в клас JavaScript няма, но клас е стойност и може да е статично поле. Така отвън той се казва Order.Line, като вложен клас (nested class) в 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("Кафе на зърна", 2, 12.5));order.add(new Order.Line("Чаша", 4, 3.0));console.log(`Общо: ${order.total().toFixed(2)} €`);Общо: 37.00 €static Line = class { ... }е статично поле наOrderсъс стойност клас без име. Отвън и вътре в методите той еOrder.Line.new Order.Line(...)се прави без обектOrder, като обикновен клас.Order.Lineне е вързан за конкретна поръчка и не вижда полетата ѝ. В Java такъв клас се казва статичен.
Такъв запис се среща по-рядко. Често малкият клас е просто до големия, в същия файл, или вместо клас има обект с полета. За това е следващата карта.
Идеята
Данни в данни: обекти и масиви
Ако редът на поръчката само пази стойности и няма методи, в JavaScript често няма клас. Поръчката е обект, в който има масив от обекти:
const order = { customer: "Иванова", lines: [ { product: "Кафе на зърна", quantity: 2, price: 12.5 }, { product: "Чаша", 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.toFixed(2)} €`);Кафе на зърнаОбщо: 37.00 €order.linesе масивът,order.lines[0]е първият ред, аorder.lines[0].productе продуктът в него. Всяка точка или[...]влиза едно ниво навътре.- Тук обектът е за известни полета (
customer,lines). За ключове, които идват по време на работа, еMap, както в урока за речниците. - Клас е по-добрият избор, когато данните имат поведение (метод
total) или правила, които трябва да се пазят.
Map от масиви събира нещата в групи:
const people = [["Иван", "София"], ["Мария", "Варна"], ["Ана", "София"]];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("София")[1]);Map(2) { 'София' => [ 'Иван', 'Ана' ], 'Варна' => [ 'Мария' ] }АнаПо-точно, за любопитните
- Капан с вложени масиви:
fillслага една и съща референция на всички места. Трите реда са един и същ масив:
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.logпоказва само няколко нива. По-дълбоко пише[Array]или[Object]. Целия обект показваJSON.stringify:
const shop = { name: "Кафене", menu: { drinks: { hot: ["кафе", "чай"] } } };console.log(shop);console.log(JSON.stringify(shop));{ name: 'Кафене', menu: { drinks: { hot: [Array] } } }{"name":"Кафене","menu":{"drinks":{"hot":["кафе","чай"]}}}Идеята
Вложеният обект пази външния
Количката има отстъпка за всички продукти в нея. Всеки продукт трябва да знае отстъпката на своята количка. В JavaScript вложеният клас не вижда обекта отвън, затова количката се подава изрично:
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("Слушалки", 80);cart.add("Калъф", 20);cart.print();Слушалки: 72.00 €Калъф: 18.00 €add подава this, а Item я пази в this.cart. finalPrice чете #discountPercent на количката, в която е. Item е написан вътре в Cart, затова вижда # полетата ѝ, както вътрешният клас в Java вижда private.const cart = new Cart(10);cart.add("Слушалки", 80);cart.add("Калъф", 20);cart.print();Стъпка по стъпка, с думи
- Количката е обект Cart с отстъпка 10%.
- add създава Item и му подава количката, this. Освен name и price, всеки Item пази в cart стрелка към своята количка.
- Вторият Item сочи към същата количка.
- finalPrice на „Слушалки“ минава по стрелката this.cart и чете #discountPercent: 80 * (100 - 10) / 100 = 72.
- finalPrice на „Калъф“ минава по стрелката this.cart и чете #discountPercent: 20 * (100 - 10) / 100 = 18.
По-точно, за любопитните
- Отвън обект
Itemсе създава с количката като първи аргумент. Обикновено външният клас сам създава вътрешните, катоaddтук:
const cart = new Cart(10);const cable = new Cart.Item(cart, "Кабел", 10);console.log(cable.finalPrice());9- Полето
cartе точно това, което в Java прави скритотоCart.this. Тук то просто се вижда. - Ако вложеният клас не ползва полетата на външния, не му давай външния обект. Така не пази излишна референция.
Пробвай
Две колички
Класът Cart е същият. Едната количка е без отстъпка, а другата е с 20% за членове на клуба.
Какво ще отпечата програмата?
const regular = new Cart(0);const member = new Cart(20);regular.add("Книга", 15);member.add("Книга", 15);regular.print();member.print();Всеки Item вижда отстъпката на количката, която го е създала. Книгата в regular е с 0%, а тази в member е с 20%.
Пробвай
Намери грешката
console.log(new Order.Line("Кафе", 12.5).show()) програмата спира:ReferenceError: currency is not definedНа кой ред е проблемът?
Избери реда с грешката.
currency е поле на обект Order, а Line няма поръчка. Само име currency JavaScript търси като променлива и не го намира. Грешката излиза чак когато show се извика, не по-рано. Поправката е Line да получи поръчката, new Order.Line(order, "Кафе", 12.5), и да чете this.order.currency. Друг начин е currency да е статично поле, Order.currency, ако валутата е една за всички поръчки.Идеята
Клас във функция
Клас може да се обяви и в функция. Тогава се вижда само там и се казва локален клас:
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(`Най-продаван: ${best.name} (${best.count} бр.)`);}const names = ["Кафе", "Чай", "Вода"];const sold = [120, 45, 300];printBestSeller(names, sold);Най-продаван: Вода (300 бр.)Sale държи заедно името и броя. Извън printBestSeller никой не го ползва, затова е там. Такива класове се срещат рядко, но е добре да ги познаваш.
Идеята
Обект вместо клас
Отстъпката в магазина е нещо с метод apply(price). В модул „Наследяване“ видя, че в JavaScript няма interface: стига обектът да има методите. За всяка отстъпка можеш да направиш клас: class BlackFriday { apply(price) { ... } }. В Java за отстъпка, която се ползва само на едно място, има анонимен клас: клас без име, написан точно там. В JavaScript обикновено не трябва и клас. Обектът с метод се пише направо:
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) { ... } }е обект с един метод. Клас няма, аblackFriday.apply(100)работи като с обект от клас.- JavaScript печата
70, а не70.0: числото е цяло.
По-точно, за любопитните
- Клас без име в JavaScript има:
class { ... }е израз и може да се създаде веднага. Пише се рядко, защото обектът с метод е по-кратък:
const none = new (class { apply(price) { return price; }})();console.log(none.apply(100));console.log(none.constructor.name === "");100true- Когато отстъпката е само едно действие, в JavaScript често се подава направо функция, както функцията за сравнение в следващата карта. В модул „Модерен JavaScript“ ще видиш по-кратък запис на малки функции.
Идеята
Функция за сравнение: подреждане по свое правило
sort подреди текст, а числа подреди с функция. С ученици без функция не дава грешка, но подрежда по текста от toString:
class Student { constructor(name, grade) { this.name = name; this.grade = grade; }
toString() { return `${this.name} ${this.grade}`; }}const students = [ new Student("Иван", 5.50), new Student("Мария", 6.00), new Student("Петър", 4.75), new Student("Ана", 5.50),];students.sort();console.log(students.join(", "));Ана 5.5, Иван 5.5, Мария 6, Петър 4.75Тук случайно излезе по име, защото toString започва с името. Без toString всеки ученик щеше да е текстът [object Object].
Правилото се дава като функция за сравнение, както при числата в урока за масивите. Тя получава двама ученици a и b и връща отрицателно число, ако a трябва да е преди b, нула, ако са равни, и положително, ако a е след b:
students.sort(function (a, b) { return b.grade - a.grade;});console.log(students.join(", "));Мария 6, Иван 5.5, Ана 5.5, Петър 4.75b.grade - a.gradeсравнява наобратно, затова най-високият успех е пръв.- Иван и Ана са с еднакъв успех и остават в реда, в който бяха.
sortне разменя равни елементи. joinвикаtoStringна всеки ученик.- За текст в обратен ред:
sort()и послеreverse().
const cities = ["Варна", "София", "Пловдив"];cities.sort();cities.reverse();console.log(cities);[ 'София', 'Пловдив', 'Варна' ]По-точно, за любопитните
sortподрежда самия масив. Ако ти трябва нов, имаtoSortedсъс същата функция.b.grade - a.gradeе безопасно, защото числата в JavaScript не препълват катоintв Java. За текст изваждане няма: там еa.name.localeCompare(b.name)от урока за текста.
Пробвай
Кое е първо?
Какво ще отпечата програмата?
const products = [ new Product("Монитор", 220), new Product("Мишка", 25), new Product("Клавиатура", 60),];
products.sort(function (a, b) { return a.price - b.price;});for (const p of products) { console.log(p.name);}a.price - b.price е отрицателно, когато a е по-евтин. Затова по-евтиният е преди: 25, 60, 220. За най-скъпото отпред размени a и b.Упражнение
Класове, които вървят заедно
Реши задачите в редактора си.
1. Количка за пазаруване. Клас ShoppingCart с вложен клас Item (име и цена, static Item = class { ... }) и методи addItem(item), getTotal() и count(). Добави new ShoppingCart.Item("Мляко", 2.5), хляб за 1,20 € и кафе за 7,50 €. Сумата печатай с toFixed(2).
Продукти: 3Общо: 11.20 €2. Монитор на уеб сървър. Клас WebServer с вложен клас Request (адрес и статус код) и методи addRequest(r) и printStats(). Статистиката казва колко са заявките, колко са от всеки статус код (подредени като числа) и какъв процент са успешни, тоест с код от 200 до 299. Броевете пази в Map. Заявките са /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Заявки: 5200: 3401: 1500: 1Успешни: 60%3. Сграден мениджър. Клас Building с вложен клас Apartment (номер, брой стаи, собственик) и методи addApartment(a) и getApartmentInfo(number). Пази апартаментите в Map номер → апартамент. Добави 101 с 3 стаи на Иванова и 102 с 2 стаи на Петров. Потърси 101 и 999.
Апартамент 101: 3 стаи, собственик ИвановаНяма апартамент 999.4. Класиране. Подреди учениците от урока по успех, от най-високия надолу. При равен успех ги подреди по име, по азбучен ред. Всичко е в една функция за сравнение: ако успехът е различен, върни b.grade - a.grade, иначе сравни имената с localeCompare.
Мария 6Ана 5.5Иван 5.5Петър 4.75Още задачи
- Конфигуратор на компютър. Клас
Computerс вложени класовеCPU(марка, честота в GHz),RAM(GB) иStorage(GB) и методиsetCPU,setRAM,setStorage.toStringописва конфигурацията:Intel 3.5 GHz, 16 GB RAM, 512 GB. - Организатор на пътуване, отново. Направи задачата от урока за
Mapведнъж с вложен класTripPlanner.Destination, който пази името и масива от дейности, и веднъж само сMapот масиви. Кое е по-удобно? - Мрежа от контакти, отново. Премести
Contactот задачата в урока заMapвътре вContactNetwork. Трябва лиContactда знае мрежата си? - Смятане наум, отново. Премести
Questionот задачата в урока за масивите вътре вMathGame. - Продукти по два начина. Подреди масив с продукти по цена от най-скъпия, а при еднаква цена по име. После подреди същия масив само по име в обратен ред.
- Своя отстъпка. Напиши три обекта с метод
apply: 10% над 100 €, фиксирани 5 € отстъпка и „втори продукт на половин цена“. Приложи ги върху една и съща количка.
Тест