Към съдържанието
wannacode

Класове в класове и данни в данни

Проблем

Клас, който има смисъл само на едно място

В урока за свързания списък 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 €
В Java има вътрешен клас (inner class): всеки обект от него пази скрита референция към обекта отвън. В JavaScript такъв няма, затова референцията е видима: add подава this, а Item я пази в this.cart. finalPrice чете #discountPercent на количката, в която е. Item е написан вътре в Cart, затова вижда # полетата ѝ, както вътрешният клас в Java вижда private.

const cart = new Cart(10);
cart.add("Слушалки", 80);
cart.add("Калъф", 20);
cart.print();
Стъпка по стъпка, с думи
  1. Количката е обект Cart с отстъпка 10%.
  2. add създава Item и му подава количката, this. Освен name и price, всеки Item пази в cart стрелка към своята количка.
  3. Вторият Item сочи към същата количка.
  4. finalPrice на „Слушалки“ минава по стрелката this.cart и чете #discountPercent: 80 * (100 - 10) / 100 = 72.
  5. 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();
Отговор

Пробвай

Намери грешката

Редът трябва да покаже валутата на поръчката. При console.log(new Order.Line("Кафе", 12.5).show()) програмата спира:
Грешка
ReferenceError: currency is not defined

На кой ред е проблемът?

Избери реда с грешката.

Идеята

Клас във функция

Клас може да се обяви и в функция. Тогава се вижда само там и се казва локален клас:

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));
Изход
70
100
  • { 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 === "");
Изход
100
true
  • Когато отстъпката е само едно действие, в 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.75
  • b.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);
}
Отговор

Упражнение

Класове, които вървят заедно

Реши задачите в редактора си.

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.

Изход
Заявки: 5
200: 3
401: 1
500: 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 € отстъпка и „втори продукт на половин цена“. Приложи ги върху една и съща количка.

Тест

Провери се

1. Как в JavaScript се прави клас, който отвън е Order.Line?
2. Вижда ли Order.Line полетата на конкретна поръчка?
3. Какво е { apply(price) { return price; } }?
4. Функцията за сравнение на sort връща отрицателно число за (a, b). Какво значи това?
5. students.sort() е без функция, а Student има toString(). Какво става?
6. grid = Array(3).fill([0, 0, 0]) и после grid[0][0] = 1. Какво има в grid?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·