Classes are templates for objects that share behaviour. The constructor sets up each new instance; methods are shared by all instances. Getters (get average()) look like properties but compute their value.
#private fields can't be read or changed from outside the class — real encapsulation. static members belong to the class itself, such as factory methods (Student.fromCSV).
extends creates a subclass; call super(...) in its constructor. Under the hood, JavaScript classes use prototypes: each object links to a prototype object that holds the shared methods, and property lookups walk up this chain.
class Student {
#scores = []; // private field
static passMark = 30;
constructor(name, form) {
this.name = name;
this.form = form;
}
addScore(score) {
if (score < 0 || score > 100) throw new RangeError("score must be 0-100");
this.#scores.push(score);
return this; // allows chaining
}
get average() {
if (this.#scores.length === 0) return 0;
return this.#scores.reduce((a, b) => a + b, 0) / this.#scores.length;
}
get passed() {
return this.average >= Student.passMark;
}
static fromCSV(line) {
const [name, form] = line.split(",");
return new Student(name.trim(), Number(form));
}
toString() {
return `${this.name} (Form ${this.form}): ${this.average.toFixed(1)}`;
}
}
class Prefect extends Student {
constructor(name, form, duty) {
super(name, form);
this.duty = duty;
}
toString() {
return `${super.toString()} - prefect for ${this.duty}`;
}
}
const amina = new Student("Amina", 4).addScore(88).addScore(79);
const neema = new Prefect("Neema", 4, "library").addScore(71);
const juma = Student.fromCSV("Juma, 3").addScore(25);
for (const s of [amina, neema, juma]) console.log(String(s), s.passed ? "PASS" : "FAIL");
console.log(neema instanceof Student); // true
console.log(Object.getPrototypeOf(neema) === Prefect.prototype); // true
console.log(Object.getPrototypeOf(Prefect.prototype) === Student.prototype); // true
console.log("scores" in amina, Object.keys(amina)); // false [ 'name', 'form' ]Key points
- Classes bundle data and behaviour; methods live on the shared prototype.
#privatefields give real encapsulation; getters compute values on access.- Prefer shallow inheritance — composition is often simpler than deep class trees.
Exercise
Create a Shape base class with an area getter and describe() method, and Circle and Rectangle subclasses. Store mixed shapes in an array, sort them by area and print each description.
Show solution
Try the exercise yourself first — then compare your approach with this one.
Shape defines the shared describe() method and expects subclasses to provide an area getter. toSorted returns a new array ordered by area, leaving the original untouched.
class Shape {
get area() {
throw new Error("Subclasses must implement area");
}
describe() {
return `${this.constructor.name} with area ${this.area.toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
get area() {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
get area() {
return this.width * this.height;
}
}
const shapes = [new Rectangle(3, 4), new Circle(1), new Rectangle(2, 2), new Circle(2)];
for (const s of shapes.toSorted((a, b) => a.area - b.area)) console.log(s.describe());
// Circle with area 3.14 / Rectangle with area 4.00 / Rectangle with area 12.00 / Circle with area 12.57