Classes inside classes and data inside data
Problem
A class that only makes sense in one place
In the lesson about the linked list, Node was a separate class. Anyone in the program can make nodes, but they only make sense in the list. The same goes for an order line:
class OrderLine { constructor(product, quantity, price) { this.product = product; this.quantity = quantity; this.price = price; }}
class Order { lines = [];}OrderLine without Order makes no sense. The name is long so it does not clash with the line of an invoice, InvoiceLine, or with the line of a warehouse. And nothing says that the two classes go together. Can the line live inside the order?
The idea
Nested class
It can. JavaScript has no class declaration inside a class, but a class is a value and can be a static field. So from the outside it is called Order.Line, like a nested class in 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("Coffee beans", 2, 12.5));order.add(new Order.Line("Mug", 4, 3.0));console.log(`Total: €${order.total().toFixed(2)}`);Total: €37.00static Line = class { ... }is a static field ofOrderwhose value is a class with no name. From the outside and inside the methods, it isOrder.Line.new Order.Line(...)is made without anOrderobject, like a regular class.Order.Lineis not tied to a specific order and does not see its fields. In Java such a class is called static.
You see this less often. Often the small class just sits next to the big one, in the same file, or there is an object with fields instead of a class. That is what the next card is about.
The idea
Data inside data: objects and arrays
If the order line only holds values and has no methods, JavaScript often has no class for it. The order is an object that holds an array of objects:
const order = { customer: "Ivanova", lines: [ { product: "Coffee beans", quantity: 2, price: 12.5 }, { product: "Mug", 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: €${total.toFixed(2)}`);Coffee beansTotal: €37.00order.linesis the array,order.lines[0]is the first line, andorder.lines[0].productis the product in it. Each dot or[...]goes one level deeper.- Here the object is for known fields (
customer,lines). For keys that come in while the program runs, there isMap, as in the lesson about maps. - A class is the better choice when the data has behavior (a
totalmethod) or rules that must be kept.
A Map of arrays collects things into groups:
const people = [["Ivan", "Sofia"], ["Maria", "Varna"], ["Ana", "Sofia"]];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("Sofia")[1]);Map(2) { 'Sofia' => [ 'Ivan', 'Ana' ], 'Varna' => [ 'Maria' ] }AnaMore precisely, if you are curious
- A trap with nested arrays:
fillputs one and the same reference in all places. The three rows are one and the same array:
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.logshows only a few levels. Deeper down it writes[Array]or[Object].JSON.stringifyshows the whole object:
const shop = { name: "Coffee Corner", menu: { drinks: { hot: ["coffee", "tea"] } } };console.log(shop);console.log(JSON.stringify(shop));{ name: 'Coffee Corner', menu: { drinks: { hot: [Array] } } }{"name":"Coffee Corner","menu":{"drinks":{"hot":["coffee","tea"]}}}The idea
The nested object keeps the outer one
The cart has a discount for all products in it. Each product must know the discount of its own cart. In JavaScript a nested class does not see the outer object, so the cart is passed explicitly:
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("Headphones", 80);cart.add("Case", 20);cart.print();Headphones: €72.00Case: €18.00add passes this, and Item keeps it in this.cart. finalPrice reads the #discountPercent of the cart it is in. Item is written inside Cart, so it sees the cart's # fields, just like an inner class in Java sees private ones.const cart = new Cart(10);cart.add("Headphones", 80);cart.add("Case", 20);cart.print();Step by step, in words
- The cart is a Cart object with a 10% discount.
- add creates an Item and passes it the cart, this. Besides name and price, each Item keeps an arrow to its own cart in cart.
- The second Item points to the same cart.
- finalPrice of "Headphones" follows the arrow this.cart and reads #discountPercent: 80 * (100 - 10) / 100 = 72.
- finalPrice of "Case" follows the arrow this.cart and reads #discountPercent: 20 * (100 - 10) / 100 = 18.
More precisely, if you are curious
- From the outside, an
Itemobject is created with the cart as the first argument. Usually the outer class creates the inner ones itself, likeaddhere:
const cart = new Cart(10);const cable = new Cart.Item(cart, "Cable", 10);console.log(cable.finalPrice());9- The
cartfield is exactly what the hiddenCart.thisdoes in Java. Here you can simply see it. - If the nested class does not use the fields of the outer class, do not give it the outer object. That way it does not keep a reference it does not need.
Try it
Two carts
The Cart class is the same. One cart has no discount, and the other has 20% for club members.
What will the program print?
const regular = new Cart(0);const member = new Cart(20);regular.add("Book", 15);member.add("Book", 15);regular.print();member.print();Each Item sees the discount of the cart that created it. The book in regular has 0%, and the one in member has 20%.
Try it
Find the bug
console.log(new Order.Line("Coffee", 12.5).show()) the program stops:ReferenceError: currency is not definedWhich line has the problem?
Pick the line with the bug.
currency is a field of an Order object, and Line has no order. JavaScript looks for a bare name currency as a variable and does not find it. The error only shows up when show is called, not earlier. The fix is for Line to get the order, new Order.Line(order, "Coffee", 12.5), and to read this.order.currency. Another way is for currency to be a static field, Order.currency, if the currency is the same for all orders.The idea
A class in a function
A class can also be declared in a function. Then it is only visible there and is called a local class:
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 seller: ${best.name} (${best.count} sold)`);}const names = ["Coffee", "Tea", "Water"];const sold = [120, 45, 300];printBestSeller(names, sold);Best seller: Water (300 sold)Sale keeps the name and the count together. Nobody uses it outside printBestSeller, so it lives there. Such classes are rare, but it is good to know them.
The idea
An object instead of a class
A discount in the shop is something with an apply(price) method. In the "Inheritance" module you saw that JavaScript has no interface: the object only needs to have the methods. You could make a class for each discount: class BlackFriday { apply(price) { ... } }. In Java, for a discount used in only one place, there is the anonymous class: a class with no name, written right there. In JavaScript you usually do not even need a class. You write the object with the method directly:
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) { ... } }is an object with one method. There is no class, andblackFriday.apply(100)works as it would with an object of a class.- JavaScript prints
70, not70.0: the number is whole.
More precisely, if you are curious
- JavaScript does have a class with no name:
class { ... }is an expression and can be created right away. It is rarely written, because the object with a method is shorter:
const none = new (class { apply(price) { return price; }})();console.log(none.apply(100));console.log(none.constructor.name === "");100true- When the discount is just one action, in JavaScript you often pass a function directly, like the compare function in the next card. In the "Modern JavaScript" module you will see a shorter way to write small functions.
The idea
Compare function: sorting by your own rule
sort sorted text, and it sorted numbers with a function. With students and no function, it gives no error, but it sorts by the text from toString:
class Student { constructor(name, grade) { this.name = name; this.grade = grade; }
toString() { return `${this.name} ${this.grade}`; }}const students = [ new Student("Ivan", 5.50), new Student("Maria", 6.00), new Student("Peter", 4.75), new Student("Ana", 5.50),];students.sort();console.log(students.join(", "));Ana 5.5, Ivan 5.5, Maria 6, Peter 4.75Here they happened to come out by name, because toString starts with the name. Without toString, each student would be the text [object Object].
The rule is given as a compare function, as with the numbers in the lesson about arrays. It gets two students a and b and returns a negative number if a should be before b, zero if they are equal, and a positive number if a is after b:
students.sort(function (a, b) { return b.grade - a.grade;});console.log(students.join(", "));Maria 6, Ivan 5.5, Ana 5.5, Peter 4.75b.grade - a.gradecompares the other way around, so the highest grade comes first.- Ivan and Ana have the same grade and stay in the order they were in.
sortdoes not swap equal elements. joincallstoStringon each student.- For text in reverse order:
sort()and thenreverse().
const cities = ["Sofia", "Varna", "Plovdiv"];cities.sort();cities.reverse();console.log(cities);[ 'Varna', 'Sofia', 'Plovdiv' ]More precisely, if you are curious
sortsorts the array itself. If you need a new one, there istoSortedwith the same function.b.grade - a.gradeis safe, because numbers in JavaScript do not overflow likeintin Java. There is no subtraction for text: there you usea.name.localeCompare(b.name)from the lesson about text.
Try it
Which one is first?
What will the program print?
const products = [ new Product("Monitor", 220), new Product("Mouse", 25), new Product("Keyboard", 60),];
products.sort(function (a, b) { return a.price - b.price;});for (const p of products) { console.log(p.name);}a.price - b.price is negative when a is cheaper. So the cheaper one comes first: 25, 60, 220. To put the most expensive first, swap a and b.Exercise
Classes that go together
Solve the tasks in your editor.
1. Shopping cart. A ShoppingCart class with a nested class Item (name and price, static Item = class { ... }) and the methods addItem(item), getTotal() and count(). Add new ShoppingCart.Item("Milk", 2.5), bread for €1.20 and coffee for €7.50. Print the total with toFixed(2).
Products: 3Total: €11.202. Web server monitor. A WebServer class with a nested class Request (address and status code) and the methods addRequest(r) and printStats(). The stats say how many requests there are, how many there are of each status code (sorted as numbers) and what percent are successful, that is, with a code from 200 to 299. Keep the counts in a Map. The requests are /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Requests: 5200: 3401: 1500: 1Successful: 60%3. Building manager. A Building class with a nested class Apartment (number, number of rooms, owner) and the methods addApartment(a) and getApartmentInfo(number). Keep the apartments in a Map from number to apartment. Add 101 with 3 rooms owned by Ivanova and 102 with 2 rooms owned by Petrov. Look up 101 and 999.
Apartment 101: 3 rooms, owner IvanovaThere is no apartment 999.4. Ranking. Sort the students from the lesson by grade, from the highest down. When the grades are equal, sort them by name, in alphabetical order. It all goes in one compare function: if the grades are different, return b.grade - a.grade, otherwise compare the names with localeCompare.
Maria 6Ana 5.5Ivan 5.5Peter 4.75More tasks
- Computer configurator. A
Computerclass with the nested classesCPU(brand, speed in GHz),RAM(GB) andStorage(GB) and the methodssetCPU,setRAM,setStorage.toStringdescribes the configuration:Intel 3.5 GHz, 16 GB RAM, 512 GB. - Trip planner, again. Do the task from the
Maplesson once with a nested classTripPlanner.Destinationthat keeps the name and the array of activities, and once with just aMapof arrays. Which one is handier? - Contact network, again. Move
Contactfrom the task in theMaplesson insideContactNetwork. ShouldContactknow its network? - Mental math, again. Move
Questionfrom the task in the lesson about arrays insideMathGame. - Products in two ways. Sort an array of products by price from the most expensive, and by name when the price is the same. Then sort the same array by name only, in reverse order.
- Your own discount. Write three objects with an
applymethod: 10% over €100, a fixed €5 off and "second product at half price". Apply them to one and the same cart.
Test