Data, switch and destructuring
Problem
The seat is taken, but Map cannot find it
Movie theater reservations: the key is the seat (row and number), the value is the person's name.
class Seat { constructor(row, number) { this.row = row; this.number = number; }}const reservations = new Map();reservations.set(new Seat(5, 12), "Maria");
console.log(reservations.get(new Seat(5, 12)));console.log(reservations.has(new Seat(5, 12)));undefinedfalseget returns undefined. new Seat(5, 12) written twice is two different objects, and a Map compares object keys by whether they are the same object. JavaScript has no methods, like equals and hashCode in Java, that let a class say when two objects are equal.The idea
A class for data and a key built from the fields
record. What Java writes by itself, here you write yourself: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(), "Maria");
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(`Row ${seat.row}, seat ${seat.number}`);MariatrueSeat(5, 12)Row 5, seat 12Object.freeze(this)at the end of the constructor forbids changing the fields.key()makes a string from the fields:"5-12". Two strings with the same characters are equal, so the string is a good key for aMap, andgetnow finds Maria.toString()says what the object looks like as text.
You read the fields directly: seat.row.
const reservations = new Map();reservations.set(new Seat(5, 12).key(), "Maria");
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(`Row ${seat.row}, seat ${seat.number}`);Step by step, in words
- An empty Map: the key is the theater seat as a string, the value is a name.
- set adds the key "5-12", built from Seat(5, 12), with the value "Maria".
- get receives the key of a new Seat object. The object is a different one, not the one from set.
- key() builds the string from the fields, so it is the same: "5-12".
- The two strings are built from equal fields: row 5 and 5, seat 12 and 12. They are equal, and get returns "Maria".
- has does the same with the key of yet another new Seat: true.
- Here toString is written by hand, and the fields are read directly: seat.row.
More precisely, if you are curious
- Changing a frozen field does not go through:
seat.rowstays 5. In a regular file there is no error, and the slip is hard to catch. The immutability is also only one level deep: an array in a field can change.
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(["coffee"]);cart.items.push("honey");console.log(cart.items);5[ 'coffee', 'honey' ]- In strict mode the same change stops with a
TypeError. Strict mode covers the code inside a class, a file that starts with"use strict";, and a module withimport:
"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]'The idea
Checks and methods
class Price { constructor(amount, currency) { if (amount < 0) { throw new RangeError(`The price cannot be negative: ${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)The price cannot be negative: -5RangeErroris the built-in kind of error for a value outside the allowed range. If the constructor throws, the object is not created.withDiscountdoes not change the price. It returns a newPrice. That is how you work with objects that do not change:coffeestays €18.50.
Try it
Are they equal?
Seat is the class with key() from the card before this one.What will the program print?
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 and b are two objects, so === is false: it asks whether they are the same object. The keys are strings built from the same fields, and they are equal. A Set, like a Map, compares objects by whether they are the same object, so both of them go in: the size is 2.Try it
The delivery comes out expensive
Delivery to an office is €4.50, but the program says €6.90. Which line is the bug on?
Pick the line with the bug.
price = 4.5; there is no break. The switch in JavaScript does not stop at the end of the case but falls through into the next one: it also runs price = 6.9;, and only then gets to break. The fix is a break after line 5. JavaScript has no switch that returns a value, but return also ends the case. That is the next card.The idea
switch with return
switch expression that gives a value. So the switch goes into a function, and every case ends with return.function deliveryPrice(method) { switch (method) { case "office": return 4.5; case "courier": case "address": return 6.9; case "store": return 0; default: throw new Error(`Unknown delivery method: ${method}`); }}
const price = deliveryPrice("office");console.log(`Delivery: €${price}`);Delivery: €4.5returnleaves the function, so after it there is no fall-through, and you do not writebreak.- Two cases with the same code are written one under the other:
case "courier":is empty and falls through intocase "address":. That is the useful kind of fall-through. defaultcatches the rest. Here it throws an exception.- JavaScript does not check that every input is covered. Without
default, no case matches an unknown method: the function reaches its end and returnsundefined, with no error:
function deliveryPrice(method) { switch (method) { case "office": return 4.5; case "courier": return 6.9; }}
console.log(deliveryPrice("post"));undefinedIf a case has several lines and its own const, it goes in { }, so they do not mix with the other cases:
function delivery(method, weightKg) { switch (method) { case "office": return 4.5; case "courier": { const base = 6.9; return weightKg > 10 ? base + 3 : base; } default: return 0; }}
console.log(delivery("courier", 4));console.log(delivery("courier", 12));6.99.9More precisely, if you are curious
When every case is just a value, you can use an object as a lookup table instead of a switch. ?? gives the fallback for a key that is not there:
const PRICES = { "office": 4.5, "courier": 6.9, "store": 0 };console.log(PRICES["courier"]);console.log(PRICES["post"] ?? "unknown method");6.9unknown methodThe idea
A family of types
The shop accepts three payment methods: card, bank transfer and cash on delivery. There are no others. Each method is its own class:
class CardPayment { constructor(cardNumber) { this.cardNumber = cardNumber; }}
class BankTransfer { constructor(iban) { this.iban = iban; }}
class CashOnDelivery {}Each payment method is just data: a card number, an IBAN or nothing. CashOnDelivery has no fields: its body is empty.
In Java, sealed makes the compiler refuse a fourth type. JavaScript has no way to say "only these three". A fourth class can show up anywhere. So the function that tells the kinds apart has to protect itself from an unknown kind. That is the next card.
See it
instanceof and destructuring
switch. The check is done with instanceof, one after another: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(`Unknown payment: ${payment.constructor.name}`);}console.log(fee(new CardPayment("4111222233334444"), 200));console.log(fee(new BankTransfer("BG80BNBG96611020345678"), 200));console.log(fee(new CashOnDelivery(), 200));30.52The throw at the end is the safeguard: if the object is none of the three kinds, the program stops with a clear message instead of quietly returning undefined:
class Voucher {}
try { fee(new Voucher(), 200);} catch (e) { console.log(`${e.name}: ${e.message}`);}TypeError: Unknown payment: VoucherThe fields are taken out with destructuring: const { iban } = payment; takes the field iban of the object and puts it in a variable with the same name. The condition for a Bulgarian account is a regular ? :.
function describe(payment) { if (payment instanceof CardPayment) { const { cardNumber } = payment; return `card ****${cardNumber.slice(-4)}`; } if (payment instanceof BankTransfer) { const { iban } = payment; return iban.startsWith("BG") ? "transfer from a Bulgarian account" : "transfer from abroad"; } if (payment instanceof CashOnDelivery) { return "cash on delivery"; } throw new TypeError(`Unknown payment: ${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()));card ****4444transfer from a Bulgarian accounttransfer from abroadcash on deliveryMore precisely, if you are curious
- When destructuring, the variable can have a different name:
const { iban: account } = transfer;. You can also do it right in the parameters of a function:({ 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));BGBGTry it
Which case wins?
What will the program print?
function label(payment, amount) { if (payment instanceof CashOnDelivery && amount > 500) { return "cash on delivery: not accepted over €500"; } if (payment instanceof CashOnDelivery) { return "cash on delivery"; } if (payment instanceof CardPayment && amount > 1000) { return "card: confirmation needed"; } if (payment instanceof CardPayment) { return "card"; } return "transfer";}
console.log(label(new CashOnDelivery(), 650));console.log(label(new CashOnDelivery(), 80));console.log(label(new CardPayment("4111222233334444"), 650));amount > 500 and gets to the second one. The card is for €650, and confirmation is needed over €1000, so the check with no condition on the amount wins: "card".More precisely, if you are curious
The check with a condition has to come before the general one for the same kind. The other way around is not an error: the general check catches everything, and the one with the condition is simply never reached:
function label(payment, amount) { if (payment instanceof CashOnDelivery) { return "cash on delivery"; } if (payment instanceof CashOnDelivery && amount > 500) { return "cash on delivery: not accepted over €500"; } return "other";}
console.log(label(new CashOnDelivery(), 650));cash on deliveryThe idea
Text on several lines
Text on several lines, like a receipt or JSON, is hard to read with " and \n on every line. A template literal between two backticks (`) can span several lines:
const receipt = `The Corner ShopCoffee beans €18.50Honey €12.00Total: €30.50`;console.log(receipt);
const name = "Maria";const total = 75.5;const json = `{"customer": "${name}", "total": ${total.toFixed(2)}}`;console.log(json);The Corner ShopCoffee beans €18.50Honey €12.00Total: €30.50{"customer": "Maria", "total": 75.50}- The text starts right after the opening backtick. That is why the first line is on the same line.
- The indentation is kept as it is written. That is why the lines here are all the way to the left.
- Regular quotes inside are not escaped.
${...}fills in the values, andtoFixed(2)gives two digits after the decimal point.
In Java, var lets the compiler infer the type of the variable. In JavaScript, var is the old way to make a variable and is not used in this course. const and let do not write a type either: the type belongs to the value, not the name.
const prices = new Map([ ["coffee", 18.5], ["honey", 12],]);for (const [name, price] of prices) { console.log(`${name}: €${price}`);}coffee: €18.5honey: €12Exercise
Data and cases
Solve the tasks in your editor.
1. Shapes. Three classes:Circle with radius, Rectangle with width and height, and Triangle with base and height. Write area(shape) with instanceof and destructuring, and throw a TypeError for an unknown kind. Also write name(shape), which says "square" for a rectangle with equal sides. Print a circle with radius 1, a 2 by 3 rectangle, a 4 by 4 square, a triangle with base 3 and height 5, and the total area.circle: 3.14rectangle: 6.00square: 16.00triangle: 7.50Total: 32.64ToOffice with office, ToAddress with city and weightKg, and Pickup with no fields, each with its own toString(). The price: to an office €4.50; to an address €5.90 in Sofia and €6.90 elsewhere, plus €1 for every kilogram over 5; pickup from the store €0. Print each delivery (with its toString()) and its price.ToOffice(Varna Center) -> €4.50ToAddress(Sofia, 2) -> €5.90ToAddress(Varna, 8) -> €9.90Pickup() -> €0.00Point with x, y and key(), which returns the string (2, 3), and count how many times they were at each point in a Map with (visits.get(key) ?? 0) + 1. At the end, look up the key of new Point(2, 3).(2, 3): 3(5, 1): 2(0, 0): 1Visits to (2, 3): 3More tasks
- A student as a data class.
Studentwithnameandgrade, whose constructor throws aRangeErrorfor a grade outside 2 to 6 and freezes the object. Sort an array of students withtoSorted((a, b) => a.grade - b.grade). - Day of the week. Rewrite the "day of the week" task from the lesson "Conditions" as a function with
switchandreturn: the cases from 1 to 5 one under the other, thenreturn "weekday";. - A letter. With a multi-line template literal, make a letter confirming a reservation: name, date, number of guests and price in euros.
Test