Classes
ES6 introduced the class syntax β a cleaner way to work with prototypal inheritance.
Class Basic Syntax
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
isAdult() {
return this.age >= 18;
}
}
let alice = new User("Alice", 30);
console.log(alice.greet()); // β "Hello, I'm Alice"
console.log(alice.isAdult()); // β true
What a Class Really Is
A class is βsyntactic sugarβ over prototypes β itβs still a function:
console.log(typeof User); // β "function"
console.log(User === User.prototype.constructor); // β true
console.log(User.prototype.greet); // β the greet method
console.log(Object.getOwnPropertyNames(User.prototype));
// β ["constructor", "greet", "isAdult"]
Class vs Function Constructor
// Class
class User {
constructor(name) { this.name = name; }
greet() { console.log("Hi"); }
}
// Equivalent function constructor
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
console.log("Hi");
};
Key Differences
class User {
constructor(name) { this.name = name; }
}
// 1. Class must be called with new
// User("Alice"); // TypeError!
// 2. Class is not hoisted
// let u = new User(); // ReferenceError (if used before definition)
// 3. Class methods are non-enumerable
console.log(Object.keys(User.prototype)); // β [] (constructor + methods not enumerable)
// 4. Class body is always in strict mode
Getters/Setters in Classes
class User {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
set name(value) {
if (value.length < 2) {
throw new Error("Name too short");
}
this._name = value;
}
}
let user = new User("Alice");
console.log(user.name); // β "Alice"
user.name = "Bob";
console.log(user.name); // β "Bob"
Computed Methods
let methodName = "greet";
class User {
constructor(name) {
this.name = name;
}
[methodName]() {
return `Hello, ${this.name}`;
}
}
let user = new User("Alice");
console.log(user.greet()); // β "Hello, Alice"
Class Fields
Class fields are properties set on individual instances (not the prototype):
class User {
name = "Anonymous"; // Class field β default value
age;
constructor(name, age) {
if (name) this.name = name;
this.age = age;
}
}
let user = new User("Alice", 30);
console.log(user.name); // β "Alice"
console.log(user.age); // β 30
let guest = new User(null, 0);
console.log(guest.name); // β "Anonymous" (default)
Class Inheritance
Use extends to inherit from another class:
class Animal {
constructor(name) {
this.name = name;
}
walk() {
return `${this.name} walks`;
}
}
class Rabbit extends Animal {
jump() {
return `${this.name} jumps`;
}
}
let rabbit = new Rabbit("Buggs");
console.log(rabbit.walk()); // β "Buggs walks" (inherited)
console.log(rabbit.jump()); // β "Buggs jumps" (own)
Overriding Methods
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
return "Some sound";
}
}
class Dog extends Animal {
makeSound() {
return "Woof!";
}
}
class Cat extends Animal {
makeSound() {
return "Meow!";
}
}
let dog = new Dog("Rex");
let cat = new Cat("Whiskers");
console.log(dog.makeSound()); // β "Woof!"
console.log(cat.makeSound()); // β "Meow!"
Using super
super is used to call parent class methods or constructors:
class Animal {
constructor(name) {
this.name = name;
}
walk() {
return `${this.name} walks`;
}
}
class Rabbit extends Animal {
constructor(name, earLength) {
super(name); // Must call parent constructor first!
this.earLength = earLength;
}
walk() {
// Extend parent method
return super.walk() + " and hops";
}
}
let rabbit = new Rabbit("Buggs", 10);
console.log(rabbit.walk()); // β "Buggs walks and hops"
Overriding Constructor
If a child class has a constructor, it must call super() before using this:
class Animal {
constructor(name) {
this.name = name;
}
}
class Rabbit extends Animal {
constructor(name, earLength) {
// this.name = name; // β Error before super()
super(name);
this.earLength = earLength; // β
OK after super()
}
}
Overriding Class Fields
class Parent {
name = "Parent";
getName() {
return this.name;
}
}
class Child extends Parent {
name = "Child";
}
let child = new Child();
console.log(child.getName()); // β "Child" (child's field overrides parent's)
Static Properties and Methods
Static methods belong to the class itself, not instances:
class MathUtils {
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
}
// Called on the class, not instances
console.log(MathUtils.add(2, 3)); // β 5
console.log(MathUtils.multiply(2, 3)); // β 6
// Cannot call on instances
let utils = new MathUtils();
// utils.add(2, 3); // TypeError!
Static Properties
class Config {
static APP_NAME = "My App";
static VERSION = "1.0.0";
static MAX_USERS = 100;
}
console.log(Config.APP_NAME); // β "My App"
console.log(Config.VERSION); // β "1.0.0"
Static Inheritance
Static properties and methods are inherited:
class Animal {
static planet = "Earth";
static identify() {
return "I am an animal";
}
}
class Rabbit extends Animal {}
console.log(Rabbit.planet); // β "Earth" (inherited)
console.log(Rabbit.identify()); // β "I am an animal" (inherited)
Use Cases for Static
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
// Factory methods
static createAdmin(name) {
return new User(name, "admin");
}
static createGuest() {
return new User("Guest", "guest");
}
// Utility
static isValidRole(role) {
return ["admin", "user", "guest"].includes(role);
}
}
let admin = User.createAdmin("Alice");
console.log(admin.name); // β "Alice"
console.log(User.isValidRole("admin")); // β true
console.log(User.isValidRole("super")); // β false
Private and Protected Properties
Protected (convention)
By convention, properties starting with _ are considered βprotectedβ β not meant to be accessed outside:
class User {
_balance = 0; // Protected by convention
getBalance() {
return this._balance;
}
deposit(amount) {
if (amount > 0) this._balance += amount;
}
}
let user = new User();
user.deposit(100);
console.log(user.getBalance()); // β 100
// console.log(user._balance); // β 100 (accessible but "please don't")
Private (genuine)
Using # prefix (ES2022) β truly private, canβt be accessed outside:
class BankAccount {
#balance = 0; // Private field
#pin;
constructor(pin) {
this.#pin = pin;
}
deposit(amount) {
if (amount > 0) this.#balance += amount;
}
#validatePin(pin) { // Private method
return this.#pin === pin;
}
withdraw(amount, pin) {
if (!this.#validatePin(pin)) {
return "Invalid PIN";
}
if (amount > this.#balance) {
return "Insufficient funds";
}
this.#balance -= amount;
return `Withdrew ${amount}. Balance: ${this.#balance}`;
}
}
let account = new BankAccount("1234");
account.deposit(500);
console.log(account.withdraw(100, "1234")); // β "Withdrew 100. Balance: 400"
// console.log(account.#balance); // SyntaxError!
// console.log(account.#pin); // SyntaxError!
Extending Built-in Classes
You can extend built-in classes like Array, Map, Error, etc.:
class PowerArray extends Array {
isEmpty() {
return this.length === 0;
}
average() {
return this.reduce((sum, n) => sum + n, 0) / this.length;
}
}
let arr = new PowerArray(1, 2, 3, 4, 5);
console.log(arr.isEmpty()); // β false
console.log(arr.average()); // β 3
console.log(arr.filter(n => n > 2).average()); // β 4.5 (returns PowerArray!)
Inheriting from Map
class DefaultMap extends Map {
constructor(defaultValue) {
super();
this.defaultValue = defaultValue;
}
get(key) {
if (!this.has(key)) {
return this.defaultValue;
}
return super.get(key);
}
}
let counts = new DefaultMap(0);
counts.set("apple", 5);
console.log(counts.get("apple")); // β 5
console.log(counts.get("banana")); // β 0 (default)
Class Checking: βinstanceofβ
Check if an object belongs to a class:
class Animal {}
class Dog extends Animal {}
let dog = new Dog();
let animal = new Animal();
console.log(dog instanceof Dog); // β true
console.log(dog instanceof Animal); // β true (inheritance)
console.log(dog instanceof Object); // β true (all objects)
console.log(animal instanceof Dog); // β false
instanceof with Constructor Functions
function Car() {}
let tesla = new Car();
console.log(tesla instanceof Car); // β true
console.log(tesla instanceof Object); // β true
Custom instanceof with Symbol.hasInstance
class Zero {
static [Symbol.hasInstance](obj) {
return obj === 0 || obj === "0";
}
}
console.log(0 instanceof Zero); // β true
console.log("0" instanceof Zero); // β true
console.log(5 instanceof Zero); // β false
Mixins
Mixins allow objects to inherit from multiple sources (JS doesnβt support multiple inheritance, but mixins are a workaround).
Simple Mixin
// Mixins as objects of methods
let sayHiMixin = {
sayHi() {
return `Hello, ${this.name}`;
}
};
let sayByeMixin = {
sayBye() {
return `Goodbye, ${this.name}`;
}
};
class User {
constructor(name) {
this.name = name;
}
}
// Copy mixin methods to prototype
Object.assign(User.prototype, sayHiMixin, sayByeMixin);
let user = new User("Alice");
console.log(user.sayHi()); // β "Hello, Alice"
console.log(user.sayBye()); // β "Goodbye, Alice"
Mixin with Inheritance
let eventMixin = {
_events: {},
on(eventName, handler) {
if (!this._events[eventName]) {
this._events[eventName] = [];
}
this._events[eventName].push(handler);
},
trigger(eventName, ...args) {
let handlers = this._events[eventName];
if (!handlers) return;
handlers.forEach(handler => handler.apply(this, args));
},
off(eventName, handler) {
let handlers = this._events[eventName];
if (!handlers) return;
this._events[eventName] = handlers.filter(h => h !== handler);
}
};
class Button {
constructor(text) {
this.text = text;
}
click() {
this.trigger("click", this.text);
}
}
Object.assign(Button.prototype, eventMixin);
let btn = new Button("Submit");
btn.on("click", (text) => console.log(`Button "${text}" clicked`));
btn.click(); // β "Button \"Submit\" clicked"
class Shape {
constructor(color) {
this.color = color;
}
describe() {
return 'A ' + this.color + ' shape';
}
static createRed() {
return new Shape('red');
}
}
class Circle extends Shape {
constructor(color, radius) {
super(color);
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
describe() {
return super.describe() + ' with radius ' + this.radius;
}
}
let redCircle = new Circle('red', 5);
document.write(redCircle.describe() + '<br>');
document.write('Area: ' + redCircle.area().toFixed(2) + '<br>');
document.write('Instanceof Circle: ' + (redCircle instanceof Circle) + '<br>');
document.write('Instanceof Shape: ' + (redCircle instanceof Shape) + '<br>');
let defaultShape = Shape.createRed();
document.write('<br>' + defaultShape.describe() + '<br>');