JavaScript — Classes & OOP
JavaScript classes, introduced in ES6 (ES2015), provide a syntactic sugar over JavaScript's existing prototype-based inheritance. While the underlying mechanism remains prototypal, the class syntax makes it easier to define constructor functions, manage inheritance, and organize code in an object-oriented style.
Classes in JavaScript are primarily syntactic sugar over the existing prototype-based inheritance model. They do not introduce a new OOP model — instead, they provide a cleaner, more declarative syntax for creating constructor functions and managing prototypes. Understanding how classes work under the hood is essential for debugging, performance optimization, and advanced patterns.
JavaScript classes are first-class citizens, support inheritance via extends, can have static members, private fields, getters/setters, and can be used in mixins. They are also strict mode by default — no need for "use strict" inside class bodies.
A class can be declared with a class declaration or a class expression. Unlike function declarations, class declarations are not hoisted — you cannot use a class before its definition in the code.
| 1 | // Class declaration (not hoisted) |
| 2 | class Rectangle { |
| 3 | constructor(width, height) { |
| 4 | this.width = width; |
| 5 | this.height = height; |
| 6 | } |
| 7 | |
| 8 | area() { |
| 9 | return this.width * this.height; |
| 10 | } |
| 11 | } |
| 12 | |
| 13 | // Class expression (anonymous or named) |
| 14 | const Circle = class { |
| 15 | constructor(radius) { |
| 16 | this.radius = radius; |
| 17 | } |
| 18 | |
| 19 | area() { |
| 20 | return Math.PI * this.radius ** 2; |
| 21 | } |
| 22 | }; |
| 23 | |
| 24 | const Square = class SquareName { |
| 25 | constructor(side) { |
| 26 | this.side = side; |
| 27 | } |
| 28 | area() { |
| 29 | return this.side ** 2; |
| 30 | } |
| 31 | }; |
| 32 | // SquareName is only visible inside the class |
| 33 | |
| 34 | // Usage |
| 35 | const rect = new Rectangle(10, 5); |
| 36 | console.log(rect.area()); // 50 |
| 37 | console.log(rect instanceof Rectangle); // true |
| 38 | |
| 39 | // Classes are first-class values |
| 40 | function createClass(className) { |
| 41 | return class { |
| 42 | constructor() { this.name = className; } |
| 43 | getName() { return this.name; } |
| 44 | }; |
| 45 | } |
| 46 | const MyDynamicClass = createClass("Dynamic"); |
| 47 | console.log(new MyDynamicClass().getName()); // "Dynamic" |
| Feature | Class Declaration | Function Constructor |
|---|---|---|
| Hoisting | Not hoisted | Hoisted |
| Strict mode | Always strict | Depends on context |
| Call without new | Throws TypeError | Creates global properties (bad) |
| Enumerable methods | Non-enumerable | Enumerable if assigned to prototype |
| typeof | "function" | "function" |
The constructor method is a special method for creating and initializing objects created with a class. Only one constructor is allowed per class. Methods defined inside the class body are added to the prototype. Static methods and fields are defined on the class itself, not on instances.
| 1 | class User { |
| 2 | // Constructor — called with new |
| 3 | constructor(name, email) { |
| 4 | this.name = name; |
| 5 | this.email = email; |
| 6 | this.createdAt = new Date(); |
| 7 | } |
| 8 | |
| 9 | // Instance method — on prototype |
| 10 | greet() { |
| 11 | return `Hello, I'm ${this.name}`; |
| 12 | } |
| 13 | |
| 14 | // Another instance method |
| 15 | updateEmail(email) { |
| 16 | const old = this.email; |
| 17 | this.email = email; |
| 18 | console.log(`Email changed from ${old} to ${email}`); |
| 19 | } |
| 20 | |
| 21 | // Get formatted info |
| 22 | getInfo() { |
| 23 | return `${this.name} <${this.email}> — joined ${this.createdAt.toLocaleDateString()}`; |
| 24 | } |
| 25 | |
| 26 | // Static method — on class, not instances |
| 27 | static createAnonymous() { |
| 28 | return new User("Anonymous", "anon@example.com"); |
| 29 | } |
| 30 | |
| 31 | // Static method for validation |
| 32 | static isValidEmail(email) { |
| 33 | return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); |
| 34 | } |
| 35 | |
| 36 | // Static field (ES2022+) |
| 37 | static defaultRole = "user"; |
| 38 | static #totalUsers = 0; // private static field |
| 39 | |
| 40 | constructor(name, email) { |
| 41 | this.name = name; |
| 42 | this.email = email; |
| 43 | User.#totalUsers++; // increment private static counter |
| 44 | } |
| 45 | |
| 46 | static getUserCount() { |
| 47 | return User.#totalUsers; |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | // Instance usage |
| 52 | const alice = new User("Alice", "alice@example.com"); |
| 53 | console.log(alice.greet()); // "Hello, I'm Alice" |
| 54 | console.log(alice.getInfo()); // "Alice <alice@example.com> — joined ..." |
| 55 | |
| 56 | // Static usage |
| 57 | const guest = User.createAnonymous(); |
| 58 | console.log(User.isValidEmail("test@test.com")); // true |
| 59 | console.log(User.isValidEmail("invalid")); // false |
| 60 | console.log(User.defaultRole); // "user" |
| 61 | console.log(User.getUserCount()); // 1 |
| 62 | |
| 63 | // Static methods are not available on instances |
| 64 | // alice.createAnonymous(); // TypeError! |
info
Private fields, denoted by the # prefix, are a native JavaScript feature (ES2022+) for true encapsulation. Unlike the older convention of prefixing with underscore (_private), # fields are genuinely inaccessible outside the class — enforced by the runtime.
| 1 | class BankAccount { |
| 2 | // Private fields (must be declared before use) |
| 3 | #balance; |
| 4 | #accountNumber; |
| 5 | static #bankName = "JS Bank"; |
| 6 | |
| 7 | constructor(owner, initialDeposit) { |
| 8 | this.owner = owner; |
| 9 | this.#balance = initialDeposit; |
| 10 | this.#accountNumber = BankAccount.#generateAccountNumber(); |
| 11 | } |
| 12 | |
| 13 | // Public methods access private fields |
| 14 | deposit(amount) { |
| 15 | if (amount <= 0) throw new Error("Invalid amount"); |
| 16 | this.#balance += amount; |
| 17 | console.log(`Deposited ${amount}. Balance: ${this.#balance}`); |
| 18 | } |
| 19 | |
| 20 | withdraw(amount) { |
| 21 | if (amount > this.#balance) throw new Error("Insufficient funds"); |
| 22 | this.#balance -= amount; |
| 23 | } |
| 24 | |
| 25 | getBalance() { |
| 26 | return this.#balance; |
| 27 | } |
| 28 | |
| 29 | // Private static method |
| 30 | static #generateAccountNumber() { |
| 31 | return "ACCT-" + Math.random().toString(36).substring(2, 10).toUpperCase(); |
| 32 | } |
| 33 | |
| 34 | static getBankName() { |
| 35 | return this.#bankName; |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | const account = new BankAccount("Alice", 1000); |
| 40 | account.deposit(500); // "Deposited 500. Balance: 1500" |
| 41 | console.log(account.getBalance()); // 1500 |
| 42 | |
| 43 | // Private fields are truly private |
| 44 | // console.log(account.#balance); // SyntaxError! |
| 45 | // console.log(account.#accountNumber); // SyntaxError! |
| 46 | // account.#balance = 0; // SyntaxError! |
| 47 | |
| 48 | // Private fields are not enumerable |
| 49 | console.log(Object.keys(account)); // ["owner"] — no private fields |
| 50 | console.log(account.hasOwnProperty("#balance")); // false |
| 51 | |
| 52 | // Private fields cannot be accessed by subclasses either |
| 53 | class SavingsAccount extends BankAccount { |
| 54 | showBalance() { |
| 55 | // console.log(this.#balance); // SyntaxError! |
| 56 | return this.getBalance(); // Must use public method |
| 57 | } |
| 58 | } |
| Aspect | _convention (old) | # private (ES2022+) |
|---|---|---|
| Enforcement | None (convention only) | Hard runtime enforcement |
| Accessible outside | Yes (obj._value) | No (SyntaxError) |
| Subclass access | Yes (inherited) | No |
| Serialization | Included in JSON | Excluded (not enumerable) |
| Performance | Same as normal property | Slightly slower (encapsulation overhead) |
warning
Getters and setters allow you to define computed properties or intercept property access on a class. They look like property access from the outside but can execute logic, validate data, or compute values on the fly.
| 1 | class Temperature { |
| 2 | constructor(celsius = 0) { |
| 3 | this._celsius = celsius; // backing store |
| 4 | } |
| 5 | |
| 6 | // Getter — accessed as a property |
| 7 | get celsius() { |
| 8 | return this._celsius; |
| 9 | } |
| 10 | |
| 11 | // Setter — assigned as a property |
| 12 | set celsius(value) { |
| 13 | if (typeof value !== "number") { |
| 14 | throw new TypeError("Temperature must be a number"); |
| 15 | } |
| 16 | if (value < -273.15) { |
| 17 | throw new RangeError("Temperature below absolute zero"); |
| 18 | } |
| 19 | this._celsius = value; |
| 20 | } |
| 21 | |
| 22 | // Computed getter |
| 23 | get fahrenheit() { |
| 24 | return this._celsius * 9 / 5 + 32; |
| 25 | } |
| 26 | |
| 27 | set fahrenheit(value) { |
| 28 | this._celsius = (value - 32) * 5 / 9; |
| 29 | } |
| 30 | |
| 31 | // Getter returning formatted string |
| 32 | get formatted() { |
| 33 | return `${this._celsius.toFixed(1)}°C / ${this.fahrenheit.toFixed(1)}°F`; |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | const temp = new Temperature(25); |
| 38 | console.log(temp.celsius); // 25 (getter) |
| 39 | console.log(temp.fahrenheit); // 77 (computed getter) |
| 40 | console.log(temp.formatted); // "25.0°C / 77.0°F" |
| 41 | |
| 42 | temp.celsius = 30; // setter |
| 43 | console.log(temp.fahrenheit); // 86 |
| 44 | |
| 45 | // temp.celsius = -300; // RangeError! |
| 46 | // temp.celsius = "hot"; // TypeError! |
| 47 | |
| 48 | // Read-only computed property (no setter) |
| 49 | class Circle { |
| 50 | constructor(radius) { |
| 51 | this.radius = radius; |
| 52 | } |
| 53 | get area() { |
| 54 | return Math.PI * this.radius ** 2; |
| 55 | } |
| 56 | get circumference() { |
| 57 | return 2 * Math.PI * this.radius; |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | const c = new Circle(5); |
| 62 | console.log(c.area); // 78.54 |
| 63 | c.area = 100; // silently ignored in non-strict, no-op |
| 64 | // Use Object.defineProperty for strict control |
best practice
The extends keyword creates a subclass that inherits from a parent class. The super keyword calls the parent constructor or accesses parent methods. JavaScript supports single inheritance — a class can extend only one other class.
| 1 | class Animal { |
| 2 | constructor(name) { |
| 3 | this.name = name; |
| 4 | } |
| 5 | |
| 6 | speak() { |
| 7 | console.log(`${this.name} makes a sound`); |
| 8 | } |
| 9 | |
| 10 | move(distance = 0) { |
| 11 | console.log(`${this.name} moved ${distance}m`); |
| 12 | } |
| 13 | |
| 14 | static classify() { |
| 15 | return "Animal"; |
| 16 | } |
| 17 | } |
| 18 | |
| 19 | // Subclass |
| 20 | class Dog extends Animal { |
| 21 | constructor(name, breed) { |
| 22 | // Must call super before accessing this |
| 23 | super(name); |
| 24 | this.breed = breed; |
| 25 | } |
| 26 | |
| 27 | // Override method |
| 28 | speak() { |
| 29 | console.log(`${this.name} barks!`); |
| 30 | } |
| 31 | |
| 32 | // Additional method |
| 33 | fetch() { |
| 34 | console.log(`${this.name} fetched the ball`); |
| 35 | } |
| 36 | |
| 37 | // Override with super call |
| 38 | move(distance = 0) { |
| 39 | console.log(`${this.breed} dog running...`); |
| 40 | super.move(distance); // call parent method |
| 41 | } |
| 42 | |
| 43 | // Static override |
| 44 | static classify() { |
| 45 | return `${super.classify()} > Mammal > Dog`; |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | const dog = new Dog("Rex", "German Shepherd"); |
| 50 | dog.speak(); // "Rex barks!" (overridden) |
| 51 | dog.move(10); // "German Shepherd dog running..." / "Rex moved 10m" |
| 52 | dog.fetch(); // "Rex fetched the ball" |
| 53 | console.log(Dog.classify()); // "Animal > Mammal > Dog" |
| 54 | |
| 55 | // instanceof |
| 56 | console.log(dog instanceof Dog); // true |
| 57 | console.log(dog instanceof Animal); // true |
| 58 | console.log(dog instanceof Object); // true (all objects) |
| 59 | |
| 60 | // Constructor chain |
| 61 | class Cat extends Animal { |
| 62 | constructor(name) { |
| 63 | // super() must be called before using this |
| 64 | super(name); |
| 65 | this.lives = 9; |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | // Error: missing super() |
| 70 | // class BadCat extends Animal { |
| 71 | // constructor(name) { |
| 72 | // // this.name = name; // ReferenceError! |
| 73 | // } |
| 74 | // } |
Method Override Patterns
| 1 | class Base { |
| 2 | greet() { |
| 3 | console.log("Base: hello"); |
| 4 | } |
| 5 | |
| 6 | save() { |
| 7 | console.log("Base: saving..."); |
| 8 | this.validate(); // virtual call — calls override |
| 9 | } |
| 10 | |
| 11 | validate() { |
| 12 | // override in subclass |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | class Derived extends Base { |
| 17 | greet() { |
| 18 | super.greet(); // call parent first |
| 19 | console.log("Derived: hello"); |
| 20 | } |
| 21 | |
| 22 | validate() { |
| 23 | if (!this.data) { |
| 24 | throw new Error("Data required"); |
| 25 | } |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | // Abstract class pattern (no native abstract in JS) |
| 30 | class AbstractDatabase { |
| 31 | constructor() { |
| 32 | if (new.target === AbstractDatabase) { |
| 33 | throw new Error("Cannot instantiate abstract class"); |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | connect() { |
| 38 | throw new Error("Subclass must implement connect()"); |
| 39 | } |
| 40 | |
| 41 | query() { |
| 42 | throw new Error("Subclass must implement query()"); |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | class MySQLDatabase extends AbstractDatabase { |
| 47 | connect() { |
| 48 | console.log("Connecting to MySQL..."); |
| 49 | } |
| 50 | query(sql) { |
| 51 | console.log(`Executing: ${sql}`); |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | // const db = new AbstractDatabase(); // Error! |
| 56 | const mysql = new MySQLDatabase(); |
| 57 | mysql.connect(); // "Connecting to MySQL..." |
pro tip
The instanceof operator checks whether an object has a constructor's prototype in its prototype chain. It works with classes and constructor functions, and respects inheritance. However, it can give false results across different execution contexts (iframes, realms).
| 1 | class Vehicle {} |
| 2 | class Car extends Vehicle {} |
| 3 | class Boat extends Vehicle {} |
| 4 | |
| 5 | const car = new Car(); |
| 6 | const boat = new Boat(); |
| 7 | |
| 8 | console.log(car instanceof Car); // true |
| 9 | console.log(car instanceof Vehicle); // true (inheritance) |
| 10 | console.log(car instanceof Boat); // false |
| 11 | console.log(car instanceof Object); // true (all objects) |
| 12 | |
| 13 | // instanceof and prototypes |
| 14 | console.log(Object.getPrototypeOf(car) === Car.prototype); // true |
| 15 | console.log(Object.getPrototypeOf(Car.prototype) === Vehicle.prototype); // true |
| 16 | |
| 17 | // Custom instanceof with Symbol.hasInstance |
| 18 | class CustomInstance { |
| 19 | static [Symbol.hasInstance](instance) { |
| 20 | return Array.isArray(instance); |
| 21 | } |
| 22 | } |
| 23 | console.log([] instanceof CustomInstance); // true |
| 24 | console.log({} instanceof CustomInstance); // false |
| 25 | |
| 26 | // instanceof pitfall: across realms |
| 27 | // An array from iframe A is not instanceof Array from iframe B |
| 28 | // Use Array.isArray() instead for cross-realm checks |
| 29 | |
| 30 | // instanceof with primitives |
| 31 | console.log("string" instanceof String); // false (primitives are not objects) |
| 32 | console.log(new String("") instanceof String); // true |
| 33 | |
| 34 | // Checking class vs function |
| 35 | class MyClass {} |
| 36 | function MyFunc() {} |
| 37 | console.log(typeof MyClass); // "function" |
| 38 | console.log(typeof MyFunc); // "function" |
info
JavaScript does not have native abstract classes, but the pattern can be implemented using new.target to prevent direct instantiation and by throwing errors in base methods that subclasses must override.
| 1 | // Abstract class pattern using new.target |
| 2 | class DataSource { |
| 3 | constructor() { |
| 4 | if (new.target === DataSource) { |
| 5 | throw new TypeError("Cannot instantiate abstract class DataSource"); |
| 6 | } |
| 7 | if (this.connect === undefined) { |
| 8 | throw new TypeError("Subclass must implement connect()"); |
| 9 | } |
| 10 | if (this.fetch === undefined) { |
| 11 | throw new TypeError("Subclass must implement fetch()"); |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | // Abstract method — must be implemented by subclass |
| 16 | connect() { |
| 17 | throw new Error("Subclass must implement connect()"); |
| 18 | } |
| 19 | |
| 20 | // Another abstract method |
| 21 | fetch() { |
| 22 | throw new Error("Subclass must implement fetch()"); |
| 23 | } |
| 24 | |
| 25 | // Concrete method — shared logic |
| 26 | getData() { |
| 27 | this.connect(); |
| 28 | return this.fetch(); |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | class APIDataSource extends DataSource { |
| 33 | connect() { |
| 34 | console.log("Connecting to API..."); |
| 35 | } |
| 36 | fetch() { |
| 37 | return { data: "from API" }; |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | class FileDataSource extends DataSource { |
| 42 | connect() { |
| 43 | console.log("Opening file..."); |
| 44 | } |
| 45 | fetch() { |
| 46 | return { data: "from file" }; |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | // const source = new DataSource(); // TypeError! |
| 51 | const api = new APIDataSource(); |
| 52 | console.log(api.getData()); // "Connecting to API..." / { data: "from API" } |
| 53 | |
| 54 | // Alternative: check in constructor pattern |
| 55 | class Shape { |
| 56 | constructor() { |
| 57 | if (this.area === undefined) { |
| 58 | throw new Error("Must implement area()"); |
| 59 | } |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | class Square extends Shape { |
| 64 | constructor(side) { |
| 65 | super(); |
| 66 | this.side = side; |
| 67 | } |
| 68 | area() { |
| 69 | return this.side ** 2; |
| 70 | } |
| 71 | } |
"Favor composition over inheritance" is a principle from the Gang of Four. Inheritance creates tight coupling between parent and child, making changes risky. Composition builds objects by combining smaller, focused behaviors, leading to more flexible and maintainable code.
| 1 | // Inheritance approach (rigid) |
| 2 | class Bird { |
| 3 | fly() { console.log("Flying..."); } |
| 4 | } |
| 5 | class Penguin extends Bird { |
| 6 | fly() { throw new Error("Penguins don't fly!"); } // Liskov violation! |
| 7 | } |
| 8 | |
| 9 | // Composition approach (flexible) |
| 10 | class FlyBehavior { |
| 11 | fly() { console.log("Flying..."); } |
| 12 | } |
| 13 | class NoFlyBehavior { |
| 14 | fly() { console.log("Can't fly"); } |
| 15 | } |
| 16 | class SwimBehavior { |
| 17 | swim() { console.log("Swimming..."); } |
| 18 | } |
| 19 | |
| 20 | class Bird2 { |
| 21 | constructor(flyBehavior, swimBehavior = null) { |
| 22 | this.flyBehavior = flyBehavior; |
| 23 | this.swimBehavior = swimBehavior; |
| 24 | } |
| 25 | fly() { return this.flyBehavior.fly(); } |
| 26 | swim() { return this.swimBehavior?.swim(); } |
| 27 | } |
| 28 | |
| 29 | const sparrow = new Bird2(new FlyBehavior()); |
| 30 | const penguin = new Bird2(new NoFlyBehavior(), new SwimBehavior()); |
| 31 | penguin.fly(); // "Can't fly" |
| 32 | penguin.swim(); // "Swimming..." |
| 33 | |
| 34 | // Mixin-like composition using Object.assign |
| 35 | const canEat = { |
| 36 | eat() { console.log(`${this.name} is eating`); } |
| 37 | }; |
| 38 | const canSleep = { |
| 39 | sleep() { console.log(`${this.name} is sleeping`); } |
| 40 | }; |
| 41 | const canSwim = { |
| 42 | swim() { console.log(`${this.name} is swimming`); } |
| 43 | }; |
| 44 | |
| 45 | class Fish { |
| 46 | constructor(name) { this.name = name; } |
| 47 | } |
| 48 | Object.assign(Fish.prototype, canEat, canSleep, canSwim); |
| 49 | |
| 50 | class Human { |
| 51 | constructor(name) { this.name = name; } |
| 52 | } |
| 53 | Object.assign(Human.prototype, canEat, canSleep); |
| 54 | |
| 55 | const nemo = new Fish("Nemo"); |
| 56 | nemo.eat(); // "Nemo is eating" |
| 57 | nemo.swim(); // "Nemo is swimming" |
best practice
Mixins are a pattern for combining behaviors from multiple sources into a single class. Since JavaScript supports single inheritance only, mixins provide a way to reuse behavior across unrelated classes. A mixin is a function that takes a base class and returns an extended class.
| 1 | // Mixin definition — function that extends a class |
| 2 | const TimestampMixin = (Base) => class extends Base { |
| 3 | constructor(...args) { |
| 4 | super(...args); |
| 5 | this.createdAt = new Date(); |
| 6 | } |
| 7 | getAge() { |
| 8 | return Date.now() - this.createdAt.getTime(); |
| 9 | } |
| 10 | }; |
| 11 | |
| 12 | const LoggableMixin = (Base) => class extends Base { |
| 13 | log(message) { |
| 14 | console.log(`[${new Date().toISOString()}] ${this.constructor.name}: ${message}`); |
| 15 | } |
| 16 | }; |
| 17 | |
| 18 | const SerializableMixin = (Base) => class extends Base { |
| 19 | toJSON() { |
| 20 | return JSON.stringify({ ...this }); |
| 21 | } |
| 22 | }; |
| 23 | |
| 24 | // Composing multiple mixins |
| 25 | class User extends LoggableMixin(TimestampMixin(SerializableMixin(class {}))) { |
| 26 | constructor(name, email) { |
| 27 | super(); |
| 28 | this.name = name; |
| 29 | this.email = email; |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | const user = new User("Alice", "alice@example.com"); |
| 34 | user.log("User created"); // "[2026-...] User: User created" |
| 35 | console.log(user.createdAt); // Date object |
| 36 | console.log(user.toJSON()); // JSON string |
| 37 | |
| 38 | // Named mixin composition helper |
| 39 | function compose(Base, ...mixins) { |
| 40 | return mixins.reduce((acc, mixin) => mixin(acc), Base); |
| 41 | } |
| 42 | |
| 43 | class Admin extends compose(class {}, TimestampMixin, LoggableMixin) { |
| 44 | constructor(name) { |
| 45 | super(); |
| 46 | this.name = name; |
| 47 | this.role = "admin"; |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | const admin = new Admin("Bob"); |
| 52 | admin.log("Admin created"); // works |
| 53 | console.log(admin.createdAt); // works |
| 54 | |
| 55 | // Mixins with shared method names — last one wins |
| 56 | const SpeakMixin = (Base) => class extends Base { |
| 57 | speak() { console.log("Generic sound"); } |
| 58 | }; |
| 59 | const BarkMixin = (Base) => class extends Base { |
| 60 | speak() { console.log("Woof!"); } |
| 61 | }; |
| 62 | |
| 63 | class Dog extends BarkMixin(SpeakMixin(class {})) {} |
| 64 | new Dog().speak(); // "Woof!" — BarkMixin overrides SpeakMixin |
warning
Understanding that classes are syntactic sugar over prototypes helps demystify JavaScript's OOP model. Every class compiles down to a constructor function and prototype assignments. Here is how the two approaches compare side by side.
| 1 | // Class syntax |
| 2 | class Person { |
| 3 | constructor(name) { |
| 4 | this.name = name; |
| 5 | } |
| 6 | greet() { |
| 7 | console.log(`Hi, I'm ${this.name}`); |
| 8 | } |
| 9 | static create(name) { |
| 10 | return new Person(name); |
| 11 | } |
| 12 | } |
| 13 | |
| 14 | // What it compiles to (approximately) |
| 15 | function PersonProto(name) { |
| 16 | this.name = name; |
| 17 | } |
| 18 | PersonProto.prototype.greet = function() { |
| 19 | console.log(`Hi, I'm ${this.name}`); |
| 20 | }; |
| 21 | PersonProto.create = function(name) { |
| 22 | return new PersonProto(name); |
| 23 | }; |
| 24 | |
| 25 | // Instantiation |
| 26 | const p1 = new Person("Alice"); |
| 27 | const p2 = new PersonProto("Bob"); |
| 28 | |
| 29 | // Methods are on prototype |
| 30 | console.log(Person.prototype.greet); // function |
| 31 | console.log(PersonProto.prototype.greet); // function |
| 32 | console.log(p1.greet === p2.greet); // false (different constructors) |
| 33 | // But: p1.greet === Person.prototype.greet // true |
| 34 | |
| 35 | // Inheritance — class vs prototype |
| 36 | class Employee extends Person { |
| 37 | constructor(name, role) { |
| 38 | super(name); |
| 39 | this.role = role; |
| 40 | } |
| 41 | work() { |
| 42 | console.log(`${this.name} works as ${this.role}`); |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | // Prototype chain equivalent |
| 47 | function EmployeeProto(name, role) { |
| 48 | PersonProto.call(this, name); |
| 49 | this.role = role; |
| 50 | } |
| 51 | Object.setPrototypeOf(EmployeeProto.prototype, PersonProto.prototype); |
| 52 | EmployeeProto.prototype.work = function() { |
| 53 | console.log(`${this.name} works as ${this.role}`); |
| 54 | }; |
| 55 | |
| 56 | // Property descriptor differences |
| 57 | // Class methods are non-enumerable |
| 58 | console.log(Object.keys(Person.prototype)); // [] (methods are non-enumerable) |
| 59 | // Prototype methods are enumerable (unless defined with defineProperty) |
| 60 | console.log(Object.keys(EmployeeProto.prototype)); // ["work"] |
| 61 | |
| 62 | // Instance properties vs prototype properties |
| 63 | class Config { |
| 64 | instances = []; // own property (per instance) |
| 65 | getConfig() {} // on prototype (shared) |
| 66 | } |
| 67 | |
| 68 | // Equivalent: |
| 69 | function ConfigProto() { |
| 70 | this.instances = []; |
| 71 | } |
| 72 | ConfigProto.prototype.getConfig = function() {}; |
| Feature | Class Syntax | Prototype Syntax |
|---|---|---|
| Readability | Clean, declarative | Verbose, manual |
| Method enumerable | Non-enumerable (correct) | Enumerable (unless definedProperty) |
| Constructor | Dedicated constructor method | Function body is constructor |
| Call without new | Throws TypeError | Pollutes global scope |
| Static methods | static keyword | Function property assignment |
| Private fields | # syntax | No native support |
| 1 | // Babel/TypeScript output example |
| 2 | // Input: class Dog extends Animal { bark() { super.speak(); } } |
| 3 | // Output (simplified): |
| 4 | function Dog(name) { |
| 5 | Animal.call(this, name); |
| 6 | } |
| 7 | Object.setPrototypeOf(Dog.prototype, Animal.prototype); |
| 8 | Dog.prototype.bark = function bark() { |
| 9 | Animal.prototype.speak.call(this); |
| 10 | }; |
| 11 | |
| 12 | // Checking the prototype chain |
| 13 | class A {} |
| 14 | class B extends A {} |
| 15 | class C extends B {} |
| 16 | |
| 17 | const c = new C(); |
| 18 | console.log(c instanceof C); // true |
| 19 | console.log(c instanceof B); // true |
| 20 | console.log(c instanceof A); // true |
| 21 | console.log(c instanceof Object); // true |
| 22 | |
| 23 | // The chain: c → C.prototype → B.prototype → A.prototype → Object.prototype |
| 24 | |
| 25 | // constructor property |
| 26 | console.log(c.constructor === C); // true |
| 27 | console.log(C.prototype.constructor === C); // true |
| 28 | |
| 29 | // __proto__ vs prototype |
| 30 | console.log(c.__proto__ === C.prototype); // true (deprecated) |
| 31 | console.log(Object.getPrototypeOf(c) === C.prototype); // true (standard) |
pro tip