Данни, switch и разглобяване
Проблем
Мястото е заето, но Map не го намира
Резервации в кино: ключът е мястото (ред и номер), стойността е името на човека.
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)));undefinedfalseget връща undefined. new Seat(5, 12) два пъти са два различни обекта, а Map сравнява ключовете-обекти по това дали са същият обект. Методи, с които класът да каже кога два обекта са равни, като equals и hashCode в Java, в 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}`);МарияtrueSeat(5, 12)Ред 5, място 12Object.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}`);Стъпка по стъпка, с думи
- Празен Map: ключът е мястото в киното като текст, стойността е име.
- set слага ключ "5-12", направен от Seat(5, 12), със стойност „Мария“.
- get получава ключа на нов обект Seat. Обектът е друг, не този от set.
- key() прави текста от полетата, затова е еднакъв: "5-12".
- Двата текста са направени от равни полета: ред 5 и 5, място 12 и 12. Равни са, и get връща „Мария“.
- has прави същото с ключа на още един нов Seat: true.
- 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)Цената не може да е отрицателна: -5RangeErrorе готовият вид грешка за стойност извън позволеното. Ако конструкторът хвърли, обектът не се създава.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);a и b са два обекта, затова === е false: той пита дали е същият обект. Ключовете са текстове от еднакви полета и са равни. Set, също като Map, сравнява обектите по това дали са същият обект, затова в него влизат и двата: размерът е 2.Пробвай
Доставката излиза скъпа
Доставката до офис е 4,50 €, а програмата казва 6,90 €. На кой ред е грешката?
Избери реда с грешката.
price = 4.5; липсва break. switch в JavaScript не спира в края на случая, а пропада в следващия: изпълнява и price = 6.9; и чак тогава стига до break. Поправката е break след ред 5. switch, който връща стойност, в JavaScript няма, но return също спира случая. Това е следващата карта.Идеята
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.99.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));30.52throw в края е защитата: ако обектът не е нито един от трите вида, програмата спира с ясно съобщение, вместо тихо да върне 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));BGBGПробвай
Кой случай печели?
Какво ще отпечата програмата?
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));amount > 500 и стига до втората. Картата е за 650 €, а потвърждение се иска над 1000 €, затова печели проверката без условие за сумата: „карта“.По-точно, за любопитните
Проверката с условие трябва да е преди общата за същия вид. Обратното не е грешка: общата проверка хваща всичко, и тази с условието просто никога не се стига:
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.64ToOffice с 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 €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 "делник";. - Писмо. С шаблонен низ на няколко реда направи писмо за потвърждение на резервация: име, дата, брой гости и цена в евро.
Тест