Skip to content
wannacode

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)}`);
Output
Total: €37.00
  • static Line = class { ... } is a static field of Order whose value is a class with no name. From the outside and inside the methods, it is Order.Line.
  • new Order.Line(...) is made without an Order object, like a regular class.
  • Order.Line is 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)}`);
Output
Coffee beans
Total: €37.00
  • order.lines is the array, order.lines[0] is the first line, and order.lines[0].product is 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 is Map, as in the lesson about maps.
  • A class is the better choice when the data has behavior (a total method) 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]);
Output
Map(2) { 'Sofia' => [ 'Ivan', 'Ana' ], 'Varna' => [ 'Maria' ] }
Ana
More precisely, if you are curious
  • A trap with nested arrays: fill puts 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);
Output
[ [ 1, 0, 0 ], [ 1, 0, 0 ], [ 1, 0, 0 ] ]
[ [ 1, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ]
  • console.log shows only a few levels. Deeper down it writes [Array] or [Object]. JSON.stringify shows the whole object:
const shop = { name: "Coffee Corner", menu: { drinks: { hot: ["coffee", "tea"] } } };
console.log(shop);
console.log(JSON.stringify(shop));
Output
{ 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();
Output
Headphones: €72.00
Case: €18.00
Java has an inner class: each object of it keeps a hidden reference to the outer object. JavaScript has no such thing, so the reference is visible: add 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
  1. The cart is a Cart object with a 10% discount.
  2. 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.
  3. The second Item points to the same cart.
  4. finalPrice of "Headphones" follows the arrow this.cart and reads #discountPercent: 80 * (100 - 10) / 100 = 72.
  5. 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 Item object is created with the cart as the first argument. Usually the outer class creates the inner ones itself, like add here:
const cart = new Cart(10);
const cable = new Cart.Item(cart, "Cable", 10);
console.log(cable.finalPrice());
Output
9
  • The cart field is exactly what the hidden Cart.this does 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();
Answer

Try it

Find the bug

The line should show the currency of the order. With console.log(new Order.Line("Coffee", 12.5).show()) the program stops:
Error
ReferenceError: currency is not defined

Which line has the problem?

Pick the line with the bug.

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);
Output
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));
Output
70
100
  • { apply(price) { ... } } is an object with one method. There is no class, and blackFriday.apply(100) works as it would with an object of a class.
  • JavaScript prints 70, not 70.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 === "");
Output
100
true
  • 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(", "));
Output
Ana 5.5, Ivan 5.5, Maria 6, Peter 4.75

Here 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(", "));
Output
Maria 6, Ivan 5.5, Ana 5.5, Peter 4.75
  • b.grade - a.grade compares 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. sort does not swap equal elements.
  • join calls toString on each student.
  • For text in reverse order: sort() and then reverse().
const cities = ["Sofia", "Varna", "Plovdiv"];
cities.sort();
cities.reverse();
console.log(cities);
Output
[ 'Varna', 'Sofia', 'Plovdiv' ]
More precisely, if you are curious
  • sort sorts the array itself. If you need a new one, there is toSorted with the same function.
  • b.grade - a.grade is safe, because numbers in JavaScript do not overflow like int in Java. There is no subtraction for text: there you use a.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);
}
Answer

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).

Output
Products: 3
Total: €11.20

2. 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.

Output
Requests: 5
200: 3
401: 1
500: 1
Successful: 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.

Output
Apartment 101: 3 rooms, owner Ivanova
There 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.

Output
Maria 6
Ana 5.5
Ivan 5.5
Peter 4.75

More tasks

  • Computer configurator. A Computer class with the nested classes CPU (brand, speed in GHz), RAM (GB) and Storage (GB) and the methods setCPU, setRAM, setStorage. toString describes the configuration: Intel 3.5 GHz, 16 GB RAM, 512 GB.
  • Trip planner, again. Do the task from the Map lesson once with a nested class TripPlanner.Destination that keeps the name and the array of activities, and once with just a Map of arrays. Which one is handier?
  • Contact network, again. Move Contact from the task in the Map lesson inside ContactNetwork. Should Contact know its network?
  • Mental math, again. Move Question from the task in the lesson about arrays inside MathGame.
  • 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 apply method: 10% over €100, a fixed €5 off and "second product at half price". Apply them to one and the same cart.

Test

Check yourself

1. How do you make a class in JavaScript that is called Order.Line from the outside?
2. Does Order.Line see the fields of a specific order?
3. What is { apply(price) { return price; } }?
4. The compare function of sort returns a negative number for (a, b). What does that mean?
5. students.sort() has no function, and Student has toString(). What happens?
6. grid = Array(3).fill([0, 0, 0]) and then grid[0][0] = 1. What is in grid?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·