Class Basics
A class is a blueprint for creating objects. The constructor is called
automatically when you use new.
class User {
constructor(name, email) {
this.name = name; // instance property
this.email = email;
}
greet() {
return `Hello, I am ${this.name}`;
}
toString() {
return `User(${this.name}, ${this.email})`;
}
}
const alice = new User("Alice", "alice@example.com");
console.log(alice.greet()); // "Hello, I am Alice"
console.log(alice.name); // "Alice"
console.log(`${alice}`); // "User(Alice, alice@example.com)"
console.log(alice instanceof User); // true
// Class expression (less common)
const Animal = class {
constructor(name) { this.name = name; }
};
// Classes are NOT hoisted like function declarations
// const x = new MyClass(); // ReferenceError
class MyClass {}
const x = new MyClass(); // OK after the declaration
Methods and Properties
class Counter {
// Class field: own property on each instance (ES2022)
count = 0;
step = 1;
constructor(start = 0, step = 1) {
this.count = start;
this.step = step;
}
// Instance method (on prototype - shared)
increment() {
this.count += this.step;
return this; // return this for method chaining
}
decrement() {
this.count -= this.step;
return this;
}
reset() {
this.count = 0;
return this;
}
value() {
return this.count;
}
// Arrow function as class field (own property - not on prototype)
// Useful when passing as callback - always has correct 'this'
handleClick = () => {
this.increment();
console.log(this.count);
}
}
const c = new Counter(0, 2);
c.increment().increment().decrement();
console.log(c.value()); // 2 (0 +2 +2 -2)
// Prototype vs instance: methods are shared, fields are per-instance
const a = new Counter();
const b = new Counter();
console.log(a.increment === b.increment); // true (shared method)
console.log(a.handleClick === b.handleClick); // false (per-instance field)
Getters and Setters
Getters and setters allow computed properties and validation on assignment. They are accessed like regular properties, not called like methods.
class Temperature {
constructor(celsius) {
this._celsius = celsius;
}
// Getter: accessed as a property, not a method call
get fahrenheit() {
return this._celsius * 9 / 5 + 32;
}
get celsius() {
return this._celsius;
}
// Setter: validation on assignment
set celsius(value) {
if (value < -273.15) {
throw new RangeError("Temperature below absolute zero");
}
this._celsius = value;
}
get description() {
if (this._celsius < 0) return "freezing";
if (this._celsius < 20) return "cold";
if (this._celsius < 30) return "warm";
return "hot";
}
}
const temp = new Temperature(100);
console.log(temp.fahrenheit); // 212 (getter - no parentheses!)
console.log(temp.description); // "hot"
temp.celsius = 20; // uses setter
console.log(temp.celsius); // 20
console.log(temp.fahrenheit); // 68
// temp.celsius = -300; // throws RangeError
Static Members
static methods and properties belong to the class itself, not to any instance.
They cannot access this (the instance) - this inside a static
method refers to the class.
class MathHelper {
static PI = 3.14159265358979;
static square(n) { return n ** 2; }
static cube(n) { return n ** 3; }
static clamp(n, min, max) { return Math.min(Math.max(n, min), max); }
}
// Call on the class, not an instance
console.log(MathHelper.PI); // 3.14159...
console.log(MathHelper.square(5)); // 25
console.log(MathHelper.clamp(15, 0, 10)); // 10
// const h = new MathHelper();
// h.square(5); // TypeError: h.square is not a function
// Factory method pattern (static that returns instances)
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
static createAdmin(name) {
return new User(name, "admin");
}
static createGuest() {
return new User("Guest", "guest");
}
}
const admin = User.createAdmin("Alice");
const guest = User.createGuest();
console.log(admin.role); // "admin"
console.log(guest.name); // "Guest"
Private Fields (#)
Private fields (prefixed with #) are truly encapsulated - inaccessible
from outside the class. They must be declared at the class body level.
class BankAccount {
#balance = 0; // private field
#owner;
constructor(owner, initialBalance = 0) {
this.#owner = owner;
this.#balance = initialBalance;
}
deposit(amount) {
if (amount <= 0) throw new Error("Amount must be positive");
this.#balance += amount;
return this;
}
withdraw(amount) {
if (amount > this.#balance) throw new Error("Insufficient funds");
this.#balance -= amount;
return this;
}
get balance() { return this.#balance; } // expose via getter
get owner() { return this.#owner; }
toString() {
return `${this.#owner}'s account: $${this.#balance}`;
}
}
const account = new BankAccount("Alice", 1000);
account.deposit(500).withdraw(200);
console.log(account.balance); // 1300
console.log(`${account}`); // "Alice's account: $1300"
// console.log(account.#balance); // SyntaxError - truly private!
// account.#balance = 9999; // SyntaxError
Inheritance (extends and super)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
toString() {
return `Animal(${this.name})`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // MUST call super() before accessing 'this'
this.breed = breed;
}
// Override parent method
speak() {
return `${this.name} barks!`;
}
// Call parent method with super.method()
describe() {
return `${super.speak()} It is a ${this.breed}.`;
}
}
class GoldenRetriever extends Dog {
constructor(name) {
super(name, "Golden Retriever"); // chain up
}
fetch() {
return `${this.name} fetches the ball!`;
}
}
const buddy = new GoldenRetriever("Buddy");
console.log(buddy.speak()); // "Buddy barks!"
console.log(buddy.describe()); // "Buddy makes a sound. It is a Golden Retriever."
console.log(buddy.fetch()); // "Buddy fetches the ball!"
console.log(buddy instanceof GoldenRetriever); // true
console.log(buddy instanceof Dog); // true
console.log(buddy instanceof Animal); // true
Method Overriding
class Shape {
constructor(color = "black") {
this.color = color;
}
area() { return 0; }
perimeter() { return 0; }
describe() {
return `${this.constructor.name}: area=${this.area().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(radius, color) {
super(color);
this.radius = radius;
}
area() { return Math.PI * this.radius ** 2; }
perimeter() { return 2 * Math.PI * this.radius; }
}
class Rectangle extends Shape {
constructor(w, h, color) {
super(color);
this.width = w;
this.height = h;
}
area() { return this.width * this.height; }
perimeter() { return 2 * (this.width + this.height); }
}
const shapes = [
new Circle(5),
new Rectangle(4, 6),
new Circle(3, "red"),
];
// Polymorphism: same method, different behavior
shapes.forEach(s => console.log(s.describe()));
// "Circle: area=78.54"
// "Rectangle: area=24.00"
// "Circle: area=28.27"
Class Patterns
// Mixin pattern (mix behaviors without multiple inheritance)
const Serializable = (Base) => class extends Base {
toJSON() { return JSON.stringify(this); }
static fromJSON(json) { return Object.assign(new this(), JSON.parse(json)); }
};
const Validatable = (Base) => class extends Base {
validate() {
for (const [key, value] of Object.entries(this)) {
if (value === null || value === undefined) {
throw new Error(`${key} is required`);
}
}
return true;
}
};
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class RichUser extends Serializable(Validatable(User)) {}
const user = new RichUser("Alice", "alice@example.com");
user.validate(); // OK
console.log(user.toJSON()); // '{"name":"Alice","email":"alice@example.com"}'