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

Данни, switch и разглобяване

Проблем

Мястото е заето, но Map не го намира

Резервации в кино: ключът е мястото (ред и номер), стойността е името на човека.

Seat
class Seat {
constructor(row, number) {
this.row = row;
this.number = number;
}
}
const reservations = new Map();
reservations.set(new Seat(5, 12), "Мария");
console.log(reservations.get(new Seat(5, 12)));
console.log(reservations.has(new Seat(5, 12)));
Изход
undefined
false
Мария е на ред 5, място 12, но get връща undefined. new Seat(5, 12) два пъти са два различни обекта, а Map сравнява ключовете-обекти по това дали са същият обект. Методи, с които класът да каже кога два обекта са равни, като equals и hashCode в Java, в JavaScript няма.

Идеята

Клас за данни и ключ от полетата

В JavaScript record няма. Каквото Java пише сама, тук пишеш ти:
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(), "Мария");
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(`Ред ${seat.row}, място ${seat.number}`);
Изход
Мария
true
Seat(5, 12)
Ред 5, място 12
  • Object.freeze(this) в края на конструктора забранява да се сменят полетата.
  • key() прави от полетата текст: "5-12". Два текста с еднакви букви са равни, затова текстът е добър ключ за Map, и get вече намира Мария.
  • toString() казва как да изглежда обектът в текст.

Полетата се четат направо: seat.row.

const reservations = new Map();
reservations.set(new Seat(5, 12).key(), "Мария");
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(`Ред ${seat.row}, място ${seat.number}`);
Стъпка по стъпка, с думи
  1. Празен Map: ключът е мястото в киното като текст, стойността е име.
  2. set слага ключ "5-12", направен от Seat(5, 12), със стойност „Мария“.
  3. get получава ключа на нов обект Seat. Обектът е друг, не този от set.
  4. key() прави текста от полетата, затова е еднакъв: "5-12".
  5. Двата текста са направени от равни полета: ред 5 и 5, място 12 и 12. Равни са, и get връща „Мария“.
  6. has прави същото с ключа на още един нов Seat: true.
  7. toString тук е написан на ръка, а полетата се четат направо: seat.row.
По-точно, за любопитните
  • Промяната на замразено поле не минава: seat.row си остава 5. В обикновен файл грешка няма и пропускът се хваща трудно. Неизменността е и само на първо ниво: масивът в поле може да се променя.
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(["кафе"]);
cart.items.push("мед");
console.log(cart.items);
Изход
5
[ 'кафе', 'мед' ]
  • В строг режим същата промяна спира с TypeError. Строг е кодът вътре в клас, файлът, който започва с "use strict";, и модулът с 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}`);
}
Изход
TypeError: Cannot assign to read only property 'row' of object '[object Object]'

Идеята

Проверка и методи

Цената не може да е отрицателна. Проверката е в началото на конструктора, преди полетата да се запишат.
class Price {
constructor(amount, currency) {
if (amount < 0) {
throw new RangeError(`Цената не може да е отрицателна: ${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)
Цената не може да е отрицателна: -5
  • RangeError е готовият вид грешка за стойност извън позволеното. Ако конструкторът хвърли, обектът не се създава.
  • withDiscount не променя цената. Връща нова Price. Така се работи с обекти, които не се променят: coffee си остава 18,50 €.

Пробвай

Равни ли са?

Seat е класът с key() от картата преди тази.

Какво ще отпечата програмата?

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);
Отговор

Пробвай

Доставката излиза скъпа

Доставката до офис е 4,50 €, а програмата казва 6,90 €. На кой ред е грешката?

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

Идеята

switch с return

Израз switch, който дава стойност, в JavaScript няма. Затова switch се слага във функция, и всеки случай завършва с return.
function deliveryPrice(method) {
switch (method) {
case "офис":
return 4.5;
case "куриер":
case "до адрес":
return 6.9;
case "магазин":
return 0;
default:
throw new Error(`Непознат начин на доставка: ${method}`);
}
}
const price = deliveryPrice("офис");
console.log(`Доставка: ${price} €`);
Изход
Доставка: 4.5 €
  • return излиза от функцията, затова след него пропадане няма и break не се пише.
  • Два случая с един и същ код се пишат един под друг: case "куриер": е празен и пропада в case "до адрес":. Това е полезното пропадане.
  • default хваща останалото. Тук хвърля изключение.
  • JavaScript не проверява, че всеки вход е покрит. Без default за непознат начин не пасва нито един случай: функцията стига до края и връща undefined, без грешка:
function deliveryPrice(method) {
switch (method) {
case "офис":
return 4.5;
case "куриер":
return 6.9;
}
}
console.log(deliveryPrice("поща"));
Изход
undefined

Ако случаят има няколко реда и свои const, той е във { }, за да не се смесват с другите случаи:

function delivery(method, weightKg) {
switch (method) {
case "офис":
return 4.5;
case "куриер": {
const base = 6.9;
return weightKg > 10 ? base + 3 : base;
}
default:
return 0;
}
}
console.log(delivery("куриер", 4));
console.log(delivery("куриер", 12));
Изход
6.9
9.9
По-точно, за любопитните

Когато всеки случай е само стойност, вместо switch може да е обект-таблица. ?? дава резервата за ключ, който го няма:

const PRICES = { "офис": 4.5, "куриер": 6.9, "магазин": 0 };
console.log(PRICES["куриер"]);
console.log(PRICES["поща"] ?? "непознат начин");
Изход
6.9
непознат начин

Идеята

Семейство от типове

Магазинът приема три начина на плащане: карта, банков превод и наложен платеж. Друг няма. Всеки начин е свой клас:

class CardPayment {
constructor(cardNumber) {
this.cardNumber = cardNumber;
}
}
class BankTransfer {
constructor(iban) {
this.iban = iban;
}
}
class CashOnDelivery {}

Всеки начин на плащане е само данни: номер на карта, IBAN или нищо. CashOnDelivery няма полета: тялото му е празно.

В Java sealed кара компилатора да откаже четвърти тип. В JavaScript начин да се каже „само тези три“ няма. Четвърти клас може да се появи навсякъде. Затова функцията, която различава видовете, сама трябва да се пази от непознат вид. Това е следващата карта.

Виж го

instanceof и разглобяване

Таксата зависи от начина на плащане. Шаблони за тип, като в switch на Java, в JavaScript няма. Проверката е с instanceof, една след друга:
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(`Непознато плащане: ${payment.constructor.name}`);
}
console.log(fee(new CardPayment("4111222233334444"), 200));
console.log(fee(new BankTransfer("BG80BNBG96611020345678"), 200));
console.log(fee(new CashOnDelivery(), 200));
Изход
3
0.5
2

throw в края е защитата: ако обектът не е нито един от трите вида, програмата спира с ясно съобщение, вместо тихо да върне undefined:

class Voucher {}
try {
fee(new Voucher(), 200);
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
Изход
TypeError: Непознато плащане: Voucher

Полетата се вадят с разглобяване (destructuring): const { iban } = payment; взима полето iban на обекта и го слага в променлива със същото име. Условието за българска сметка е обикновен ? :.

function describe(payment) {
if (payment instanceof CardPayment) {
const { cardNumber } = payment;
return `карта ****${cardNumber.slice(-4)}`;
}
if (payment instanceof BankTransfer) {
const { iban } = payment;
return iban.startsWith("BG") ? "превод от българска сметка" : "превод от чужбина";
}
if (payment instanceof CashOnDelivery) {
return "наложен платеж";
}
throw new TypeError(`Непознато плащане: ${payment.constructor.name}`);
}
console.log(describe(new CardPayment("4111222233334444")));
console.log(describe(new BankTransfer("BG80BNBG96611020345678")));
console.log(describe(new BankTransfer("DE89370400440532013000")));
console.log(describe(new CashOnDelivery()));
Изход
карта ****4444
превод от българска сметка
превод от чужбина
наложен платеж
По-точно, за любопитните
  • При разглобяване променливата може да е с друго име: const { iban: account } = transfer;. Може и направо в параметрите на функция: ({ iban }) => ....
const transfer = new BankTransfer("BG80BNBG96611020345678");
const { iban: account } = transfer;
console.log(account.slice(0, 2));
const country = ({ iban }) => iban.slice(0, 2);
console.log(country(transfer));
Изход
BG
BG

Пробвай

Кой случай печели?

Проверките вървят отгоре надолу. Печели първата, която е вярна.

Какво ще отпечата програмата?

function label(payment, amount) {
if (payment instanceof CashOnDelivery && amount > 500) {
return "наложен платеж: не се приема над 500 €";
}
if (payment instanceof CashOnDelivery) {
return "наложен платеж";
}
if (payment instanceof CardPayment && amount > 1000) {
return "карта: нужно е потвърждение";
}
if (payment instanceof CardPayment) {
return "карта";
}
return "превод";
}
console.log(label(new CashOnDelivery(), 650));
console.log(label(new CashOnDelivery(), 80));
console.log(label(new CardPayment("4111222233334444"), 650));
Отговор
По-точно, за любопитните

Проверката с условие трябва да е преди общата за същия вид. Обратното не е грешка: общата проверка хваща всичко, и тази с условието просто никога не се стига:

function label(payment, amount) {
if (payment instanceof CashOnDelivery) {
return "наложен платеж";
}
if (payment instanceof CashOnDelivery && amount > 500) {
return "наложен платеж: не се приема над 500 €";
}
return "друго";
}
console.log(label(new CashOnDelivery(), 650));
Изход
наложен платеж

Идеята

Текст на няколко реда

Текст на няколко реда, като касова бележка или JSON, с " и \n на всеки ред се чете трудно. Шаблонният низ между две обратни кавички (`) може да е на няколко реда:

const receipt = `Магазин „Ъгъла“
Кафе на зърна 18.50 €
Мед 12.00 €
Общо: 30.50 €`;
console.log(receipt);
const name = "Мария";
const total = 75.5;
const json = `{"customer": "${name}", "total": ${total.toFixed(2)}}`;
console.log(json);
Изход
Магазин „Ъгъла“
Кафе на зърна 18.50 €
Мед 12.00 €
Общо: 30.50 €
{"customer": "Мария", "total": 75.50}
  • Текстът започва веднага след отварящата кавичка. Затова първият ред е на същия ред.
  • Отстъпът се пази, както е написан. Затова тук редовете са най-вляво.
  • Обикновените кавички вътре не се екранират.
  • ${...} попълва стойностите, а toFixed(2) дава два знака след точката.

В Java var оставя компилатора да изведе типа на променливата. В JavaScript var е старият начин да се направи променлива и в този курс не се ползва. const и let също не пишат тип: типът е на стойността, не на името.

const prices = new Map([
["кафе", 18.5],
["мед", 12],
]);
for (const [name, price] of prices) {
console.log(`${name}: ${price} €`);
}
Изход
кафе: 18.5 €
мед: 12 €

Упражнение

Данни и случаи

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

1. Фигури. Три класа: Circle с radius, Rectangle с width и height и Triangle с base и height. Напиши area(shape) с instanceof и разглобяване, а за непознат вид хвърли TypeError. Напиши и name(shape), който за правоъгълник с равни страни казва „квадрат“. Отпечатай кръг с радиус 1, правоъгълник 2 на 3, квадрат 4 на 4, триъгълник с основа 3 и височина 5 и общата площ.
Изход
кръг: 3.14
правоъгълник: 6.00
квадрат: 16.00
триъгълник: 7.50
Общо: 32.64
2. Цена на доставка. Три класа: ToOffice с office, ToAddress с city и weightKg и Pickup без полета, всеки със свой toString(). Цената: до офис 4,50 €; до адрес 5,90 € в София и 6,90 € другаде, плюс 1 € за всеки килограм над 5; от магазина 0 €. Отпечатай всяка доставка (с нейния toString()) и цената ѝ.
Изход
ToOffice(Варна Център) -> 4.50 €
ToAddress(София, 2) -> 5.90 €
ToAddress(Варна, 8) -> 9.90 €
Pickup() -> 0.00 €
3. Точка като ключ. Куриерът минава през точки по картата: (2, 3), (5, 1), (2, 3), (0, 0), (5, 1), (2, 3). Направи клас Point с x, y и key(), който връща текста (2, 3), и преброй колко пъти е бил във всяка точка в Map с (visits.get(key) ?? 0) + 1. Накрая потърси ключа на new Point(2, 3).
Изход
(2, 3): 3
(5, 1): 2
(0, 0): 1
Посещения в (2, 3): 3

Още задачи

  • Студент като клас за данни. Student с name и grade, чийто конструктор хвърля RangeError за оценка извън 2–6 и замразява обекта. Подреди масив от студенти с toSorted((a, b) => a.grade - b.grade).
  • Ден от седмицата. Пренапиши задачата „ден от седмицата“ от урока „Условия“ като функция със switch и return: случаите от 1 до 5 един под друг, после return "делник";.
  • Писмо. С шаблонен низ на няколко реда направи писмо за потвърждение на резервация: име, дата, брой гости и цена в евро.

Тест

Провери се

1. const m = new Map(); m.set(new Seat(5, 12), "Мария"); Какво дава m.get(new Seat(5, 12))?
2. Какво прави Object.freeze(this) в края на конструктора?
3. Какво става в switch, ако в един case липсват и break, и return?
4. Какво прави const { iban } = payment;?
5. В JavaScript няма sealed. Как функцията, която различава видовете плащане, се пази от непознат вид?

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