Warm-up · Activity 1 of 7
Warm-up from lesson I2.3. What does this print?
class Base {
hello() {
return "base";
}
}
class Child extends Base {
hello() {
return `child + ${super.hello()}`;
}
}
console.log(new Child().hello());// I2.4 · ~34 min · Intermediate
After this lesson you protect a class’s data with #private fields, offer it through getters and setters that check new values, and add static methods such as a factory.
Warm-up · Activity 1 of 7
class Base {
hello() {
return "base";
}
}
class Child extends Base {
hello() {
return `child + ${super.hello()}`;
}
}
console.log(new Child().hello());Predict · Activity 2 of 7
class Counter {
#count = 0;
increment() {
this.#count += 1;
return this.#count;
}
}
const c = new Counter();
c.increment();
c.count = 100;
console.log(c.increment(), Object.keys(c).join(","));Practice · Activity 3 of 7
class Cart {
#prices = [3, 4];
____ total() {
return this.#prices.reduce((sum, price) => sum + price, 0);
}
}
console.log(new Cart().total);Practice · Activity 4 of 7
Practice · Activity 5 of 7
class Temp {
constructor(celsius) {
this.celsius = celsius;
}
static fromFahrenheit(f) {
return new Temp(((f - 32) * 5) / 9);
}
}
const t = Temp.fromFahrenheit(212);
console.log(t.celsius, typeof t.fromFahrenheit, typeof Temp.fromFahrenheit);Brain teaser · Activity 6 of 7
class Ticket {
#id;
constructor(id) {
this.#id = id;
}
static isTicket(value) {
return #id in value;
}
}
const fake = {id: 1};
console.log(Ticket.isTicket(new Ticket(1)), Ticket.isTicket(fake), "#id" in new Ticket(2));Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
Account keeps the owner and the balance in private fields. A setter cleans and checks the owner, a getter makes the balance read-only, and the private #check(amount) refuses amounts that are not positive. Static members belong to the class: a currency, a counter, a factory that builds an account from JSON, and isAccount, which uses #balance in value. The loop tries four forbidden things.
main.js
class Account {
static currency = "EUR";
static count = 0;
#owner;
#balance = 0;
constructor(owner) {
this.owner = owner; // runs the setter below
Account.count += 1;
}
static fromJSON(text) {
const data = JSON.parse(text);
const account = new Account(data.owner);
account.deposit(data.balance);
return account;
}
static isAccount(value) {
return #balance in value;
}
get owner() {
return this.#owner;
}
set owner(name) {
if (typeof name !== "string" || name.trim() === "") {
throw new TypeError("owner must be a non-empty string");
}
this.#owner = name.trim();
}
get balance() {
return this.#balance;
}
deposit(amount) {
this.#check(amount);
this.#balance += amount;
}
withdraw(amount) {
this.#check(amount);
if (amount > this.#balance) throw new RangeError("not enough money");
this.#balance -= amount;
}
#check(amount) {
if (!(amount > 0)) throw new RangeError(`invalid amount: ${amount}`);
}
}
const ada = new Account(" Ada ");
ada.deposit(100);
ada.withdraw(30);
console.log(ada.owner, ada.balance, Account.currency);
console.log(ada, Object.keys(ada));
for (const attempt of [() => (ada.balance = 1000000), () => ada.withdraw(500), () => ada.deposit(-5), () => (ada.owner = " ")]) {
try {
attempt();
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
}
const max = Account.fromJSON('{"owner": "Max", "balance": 50}');
console.log(max.owner, max.balance, Account.count);
console.log(Account.isAccount(max), Account.isAccount({balance: 50}));
console.log(typeof ada.fromJSON);
Run it with
node main.jsOutput
Ada 70 EUR
Account {} []
TypeError: Cannot set property balance of #<Account> which has only a getter
RangeError: not enough money
RangeError: invalid amount: -5
TypeError: owner must be a non-empty string
Max 50 2
true false
undefinedTab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Exercise 1 of 2
Temperature should keep its value in a private #celsius field, behind a getter and a setter called celsius. The setter throws a RangeError with the message below absolute zero for values under -273.15, also when the constructor is given one. The fahrenheit getter already works. The starter stores celsius as a public property and checks nothing.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Run the tests: -300 is accepted, and Object.keys finds celsius. Where should the value live instead?
Declare #celsius; in the class body. get celsius() returns it; set celsius(value) checks value and then stores it.
In the setter: if (value < -273.15) throw new RangeError("below absolute zero"); this.#celsius = value;
One way to solve it. Yours can look different and still pass the checks.
export class Temperature {
#celsius;
constructor(celsius) {
this.celsius = celsius;
}
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (value < -273.15) throw new RangeError("below absolute zero");
this.#celsius = value;
}
get fahrenheit() {
return (this.celsius * 9) / 5 + 32;
}
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
export class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
get fahrenheit() {
return (this.celsius * 9) / 5 + 32;
}
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {Temperature} from './main.js';
test('celsius and fahrenheit can be read', () => {
const t = new Temperature(100);
assert.deepEqual([t.celsius, t.fahrenheit], [100, 212], `celsius and fahrenheit are ${t.celsius} and ${t.fahrenheit}`);
});
test('Setting a value below -273.15 throws and keeps the old value', () => {
const t = new Temperature(20);
assert.throws(() => { t.celsius = -300; }, {name: 'RangeError', message: 'below absolute zero'}, 'setting -300 should throw a RangeError');
assert.equal(t.celsius, 20, `after the refused value celsius is ${t.celsius}`);
});
test('The constructor refuses -300 too', () => {
assert.throws(() => new Temperature(-300), {name: 'RangeError'}, 'new Temperature(-300) should throw a RangeError');
});
test('The value is not a public own property', () => {
assert.deepEqual(Object.keys(new Temperature(5)), [], 'Object.keys should find nothing: keep the value in #celsius');
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 2
Color.fromHex("#ff8000") should build a Color from a hex text: the pairs ff, 80 and 00 are red, green and blue in base 16. The starter has fromHex as an ordinary method, so you would need a colour to make a colour. Make it static. Then add a static field WHITE, a Color with 255 for all three parts.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Run the tests: Color.fromHex is undefined. Which keyword puts a method on the class itself?
Write static before fromHex(hex). A static field looks like static NAME = value;
static WHITE = new Color(255, 255, 255);
One way to solve it. Yours can look different and still pass the checks.
export class Color {
constructor(r, g, b) {
this.r = r;
this.g = g;
this.b = b;
}
static WHITE = new Color(255, 255, 255);
static fromHex(hex) {
const part = (start) => parseInt(hex.slice(start, start + 2), 16);
return new Color(part(1), part(3), part(5));
}
toString() {
return `rgb(${this.r}, ${this.g}, ${this.b})`;
}
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
export class Color {
constructor(r, g, b) {
this.r = r;
this.g = g;
this.b = b;
}
fromHex(hex) {
const part = (start) => parseInt(hex.slice(start, start + 2), 16);
return new Color(part(1), part(3), part(5));
}
toString() {
return `rgb(${this.r}, ${this.g}, ${this.b})`;
}
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {Color} from './main.js';
test('Color.fromHex builds a Color', () => {
assert.equal(typeof Color.fromHex, 'function', 'Color.fromHex should be a static method');
const c = Color.fromHex('#ff8000');
assert.ok(c instanceof Color, 'fromHex should return a Color');
assert.equal(String(c), 'rgb(255, 128, 0)', `fromHex('#ff8000') gave ${c}`);
});
test('fromHex is not a method of a colour', () => {
assert.equal(typeof new Color(0, 0, 0).fromHex, 'undefined', 'fromHex should be on the class, not on each colour');
});
test('Color.WHITE is white', () => {
assert.equal(String(Color.WHITE), 'rgb(255, 255, 255)', `Color.WHITE is ${Color.WHITE}`);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsclass Account {
#balance = 50;
}
const account = new Account();
console.log(account.#balance);
What Node.js prints
SyntaxError: Private field '#balance' must be declared in an enclosing classWhy, and the fix
A #name exists only inside the class body that declares it. Outside, the code is not even valid, so the whole file fails to start, not just this line. Give the class a way to share the value, such as get balance() { return this.#balance; }, and read account.balance.
class Cart {
constructor(prices) {
this.prices = prices;
}
get total() {
return this.total ?? this.prices.reduce((sum, p) => sum + p, 0);
}
}
console.log(new Cart([3, 4]).total);
What Node.js prints
RangeError: Maximum call stack size exceededWhy, and the fix
Inside get total(), this.total reads the property total again, which calls the same getter, again and again until the call stack is full. A getter must read something else: compute the value, or keep it in a field with another name, usually a private one such as #total.
class Account {
constructor(owner) {
this.owner = owner;
}
static fromJSON(text) {
return new Account(JSON.parse(text).owner);
}
}
const ada = new Account("Ada");
const max = ada.fromJSON('{"owner": "Max"}');
console.log(max.owner);
What Node.js prints
TypeError: ada.fromJSON is not a functionWhy, and the fix
A static method lives on the class, not on Account.prototype, so instances do not inherit it and ada.fromJSON is undefined. Call it on the class: Account.fromJSON('{"owner": "Max"}').
JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.