|$ curl https://forge-ai.dev/api/markdown?path=docs/js/classes
$cat docs/javascript-—-classes-&-oop.md
updated Recently·40 min read·published

JavaScript — Classes & OOP

JavaScriptClassesOOPAdvancedIntermediate to Advanced
Introduction

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.

Class Declaration Syntax

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.

class-declaration.js
JavaScript
1// Class declaration (not hoisted)
2class 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)
14const Circle = class {
15 constructor(radius) {
16 this.radius = radius;
17 }
18
19 area() {
20 return Math.PI * this.radius ** 2;
21 }
22};
23
24const 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
35const rect = new Rectangle(10, 5);
36console.log(rect.area()); // 50
37console.log(rect instanceof Rectangle); // true
38
39// Classes are first-class values
40function createClass(className) {
41 return class {
42 constructor() { this.name = className; }
43 getName() { return this.name; }
44 };
45}
46const MyDynamicClass = createClass("Dynamic");
47console.log(new MyDynamicClass().getName()); // "Dynamic"
FeatureClass DeclarationFunction Constructor
HoistingNot hoistedHoisted
Strict modeAlways strictDepends on context
Call without newThrows TypeErrorCreates global properties (bad)
Enumerable methodsNon-enumerableEnumerable if assigned to prototype
typeof"function""function"
Constructor, Methods & Static Members

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.

constructor-methods.js
JavaScript
1class 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
52const alice = new User("Alice", "alice@example.com");
53console.log(alice.greet()); // "Hello, I'm Alice"
54console.log(alice.getInfo()); // "Alice <alice@example.com> — joined ..."
55
56// Static usage
57const guest = User.createAnonymous();
58console.log(User.isValidEmail("test@test.com")); // true
59console.log(User.isValidEmail("invalid")); // false
60console.log(User.defaultRole); // "user"
61console.log(User.getUserCount()); // 1
62
63// Static methods are not available on instances
64// alice.createAnonymous(); // TypeError!

info

Static methods are often used for utility functions related to the class — factory methods (createAnonymous), validation (isValidEmail), or singleton access (getInstance). Static fields are useful for configuration, counters, or shared state across all instances.
Private Fields (#)

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.

private-fields.js
JavaScript
1class 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
39const account = new BankAccount("Alice", 1000);
40account.deposit(500); // "Deposited 500. Balance: 1500"
41console.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
49console.log(Object.keys(account)); // ["owner"] — no private fields
50console.log(account.hasOwnProperty("#balance")); // false
51
52// Private fields cannot be accessed by subclasses either
53class 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+)
EnforcementNone (convention only)Hard runtime enforcement
Accessible outsideYes (obj._value)No (SyntaxError)
Subclass accessYes (inherited)No
SerializationIncluded in JSONExcluded (not enumerable)
PerformanceSame as normal propertySlightly slower (encapsulation overhead)

warning

Private fields (#) are not supported in older environments (Node.js < 12, older browsers). They also cannot be accessed by subclasses — if you need protected access, consider using WeakMap-based privacy or TypeScript's private keyword (which is compile-time only and does not provide runtime enforcement).
Getters & Setters

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.

getters-setters.js
JavaScript
1class 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
37const temp = new Temperature(25);
38console.log(temp.celsius); // 25 (getter)
39console.log(temp.fahrenheit); // 77 (computed getter)
40console.log(temp.formatted); // "25.0°C / 77.0°F"
41
42temp.celsius = 30; // setter
43console.log(temp.fahrenheit); // 86
44
45// temp.celsius = -300; // RangeError!
46// temp.celsius = "hot"; // TypeError!
47
48// Read-only computed property (no setter)
49class 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
61const c = new Circle(5);
62console.log(c.area); // 78.54
63c.area = 100; // silently ignored in non-strict, no-op
64// Use Object.defineProperty for strict control

best practice

Use getters for computed properties that depend on internal state. Avoid getters with side effects — they should behave like properties, not methods. A getter that logs, mutates state, or throws (beyond type validation) violates the Principle of Least Surprise. Similarly, setters are valuable for validation but should not have unexpected side effects beyond updating internal state.
Inheritance (extends & super)

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.

inheritance.js
JavaScript
1class 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
20class 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
49const dog = new Dog("Rex", "German Shepherd");
50dog.speak(); // "Rex barks!" (overridden)
51dog.move(10); // "German Shepherd dog running..." / "Rex moved 10m"
52dog.fetch(); // "Rex fetched the ball"
53console.log(Dog.classify()); // "Animal > Mammal > Dog"
54
55// instanceof
56console.log(dog instanceof Dog); // true
57console.log(dog instanceof Animal); // true
58console.log(dog instanceof Object); // true (all objects)
59
60// Constructor chain
61class 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

override-patterns.js
JavaScript
1class 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
16class 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)
30class 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
46class 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!
56const mysql = new MySQLDatabase();
57mysql.connect(); // "Connecting to MySQL..."
🔥

pro tip

While super is powerful, deep inheritance hierarchies are hard to maintain — the "fragile base class" problem. Prefer composition over inheritance and keep your hierarchy at most 2–3 levels deep. Use super primarily for calling parent constructors and occasionally for template method patterns.
instanceof Operator

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).

instanceof.js
JavaScript
1class Vehicle {}
2class Car extends Vehicle {}
3class Boat extends Vehicle {}
4
5const car = new Car();
6const boat = new Boat();
7
8console.log(car instanceof Car); // true
9console.log(car instanceof Vehicle); // true (inheritance)
10console.log(car instanceof Boat); // false
11console.log(car instanceof Object); // true (all objects)
12
13// instanceof and prototypes
14console.log(Object.getPrototypeOf(car) === Car.prototype); // true
15console.log(Object.getPrototypeOf(Car.prototype) === Vehicle.prototype); // true
16
17// Custom instanceof with Symbol.hasInstance
18class CustomInstance {
19 static [Symbol.hasInstance](instance) {
20 return Array.isArray(instance);
21 }
22}
23console.log([] instanceof CustomInstance); // true
24console.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
31console.log("string" instanceof String); // false (primitives are not objects)
32console.log(new String("") instanceof String); // true
33
34// Checking class vs function
35class MyClass {}
36function MyFunc() {}
37console.log(typeof MyClass); // "function"
38console.log(typeof MyFunc); // "function"

info

Use instanceof for checking class membership within a single execution context. For cross-realm checks (e.g., arrays from different iframes), use the specific static method (Array.isArray(), Number.isInteger()). Avoid instanceof for primitive wrapper types — always check primitives with typeof.
Abstract Class Pattern

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.

abstract-class.js
JavaScript
1// Abstract class pattern using new.target
2class 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
32class APIDataSource extends DataSource {
33 connect() {
34 console.log("Connecting to API...");
35 }
36 fetch() {
37 return { data: "from API" };
38 }
39}
40
41class 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!
51const api = new APIDataSource();
52console.log(api.getData()); // "Connecting to API..." / { data: "from API" }
53
54// Alternative: check in constructor pattern
55class Shape {
56 constructor() {
57 if (this.area === undefined) {
58 throw new Error("Must implement area()");
59 }
60 }
61}
62
63class Square extends Shape {
64 constructor(side) {
65 super();
66 this.side = side;
67 }
68 area() {
69 return this.side ** 2;
70 }
71}
Composition Over Inheritance

"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.

composition.js
JavaScript
1// Inheritance approach (rigid)
2class Bird {
3 fly() { console.log("Flying..."); }
4}
5class Penguin extends Bird {
6 fly() { throw new Error("Penguins don't fly!"); } // Liskov violation!
7}
8
9// Composition approach (flexible)
10class FlyBehavior {
11 fly() { console.log("Flying..."); }
12}
13class NoFlyBehavior {
14 fly() { console.log("Can't fly"); }
15}
16class SwimBehavior {
17 swim() { console.log("Swimming..."); }
18}
19
20class 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
29const sparrow = new Bird2(new FlyBehavior());
30const penguin = new Bird2(new NoFlyBehavior(), new SwimBehavior());
31penguin.fly(); // "Can't fly"
32penguin.swim(); // "Swimming..."
33
34// Mixin-like composition using Object.assign
35const canEat = {
36 eat() { console.log(`${this.name} is eating`); }
37};
38const canSleep = {
39 sleep() { console.log(`${this.name} is sleeping`); }
40};
41const canSwim = {
42 swim() { console.log(`${this.name} is swimming`); }
43};
44
45class Fish {
46 constructor(name) { this.name = name; }
47}
48Object.assign(Fish.prototype, canEat, canSleep, canSwim);
49
50class Human {
51 constructor(name) { this.name = name; }
52}
53Object.assign(Human.prototype, canEat, canSleep);
54
55const nemo = new Fish("Nemo");
56nemo.eat(); // "Nemo is eating"
57nemo.swim(); // "Nemo is swimming"

best practice

Inheritance models "is-a" relationships (a Dog is an Animal). Composition models "has-a" or "can-do" relationships (a Bird can Fly, a Penguin can Swim). When in doubt, prefer composition. Inheritance is best for shared interface contracts and when subclasses truly specialize parent behavior without breaking it.
Mixins

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.

mixins.js
JavaScript
1// Mixin definition — function that extends a class
2const 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
12const LoggableMixin = (Base) => class extends Base {
13 log(message) {
14 console.log(`[${new Date().toISOString()}] ${this.constructor.name}: ${message}`);
15 }
16};
17
18const SerializableMixin = (Base) => class extends Base {
19 toJSON() {
20 return JSON.stringify({ ...this });
21 }
22};
23
24// Composing multiple mixins
25class User extends LoggableMixin(TimestampMixin(SerializableMixin(class {}))) {
26 constructor(name, email) {
27 super();
28 this.name = name;
29 this.email = email;
30 }
31}
32
33const user = new User("Alice", "alice@example.com");
34user.log("User created"); // "[2026-...] User: User created"
35console.log(user.createdAt); // Date object
36console.log(user.toJSON()); // JSON string
37
38// Named mixin composition helper
39function compose(Base, ...mixins) {
40 return mixins.reduce((acc, mixin) => mixin(acc), Base);
41}
42
43class Admin extends compose(class {}, TimestampMixin, LoggableMixin) {
44 constructor(name) {
45 super();
46 this.name = name;
47 this.role = "admin";
48 }
49}
50
51const admin = new Admin("Bob");
52admin.log("Admin created"); // works
53console.log(admin.createdAt); // works
54
55// Mixins with shared method names — last one wins
56const SpeakMixin = (Base) => class extends Base {
57 speak() { console.log("Generic sound"); }
58};
59const BarkMixin = (Base) => class extends Base {
60 speak() { console.log("Woof!"); }
61};
62
63class Dog extends BarkMixin(SpeakMixin(class {})) {}
64new Dog().speak(); // "Woof!" — BarkMixin overrides SpeakMixin

warning

Mixins can cause naming collisions if two mixins define the same method — the last mixin in the chain wins. Name your mixin methods carefully and consider using symbols as method keys to avoid collisions. Also, super chains in mixins can get complex; test thoroughly when composing multiple mixins.
Class vs Prototype Comparison

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.

class-vs-prototype.js
JavaScript
1// Class syntax
2class 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)
15function PersonProto(name) {
16 this.name = name;
17}
18PersonProto.prototype.greet = function() {
19 console.log(`Hi, I'm ${this.name}`);
20};
21PersonProto.create = function(name) {
22 return new PersonProto(name);
23};
24
25// Instantiation
26const p1 = new Person("Alice");
27const p2 = new PersonProto("Bob");
28
29// Methods are on prototype
30console.log(Person.prototype.greet); // function
31console.log(PersonProto.prototype.greet); // function
32console.log(p1.greet === p2.greet); // false (different constructors)
33// But: p1.greet === Person.prototype.greet // true
34
35// Inheritance — class vs prototype
36class 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
47function EmployeeProto(name, role) {
48 PersonProto.call(this, name);
49 this.role = role;
50}
51Object.setPrototypeOf(EmployeeProto.prototype, PersonProto.prototype);
52EmployeeProto.prototype.work = function() {
53 console.log(`${this.name} works as ${this.role}`);
54};
55
56// Property descriptor differences
57// Class methods are non-enumerable
58console.log(Object.keys(Person.prototype)); // [] (methods are non-enumerable)
59// Prototype methods are enumerable (unless defined with defineProperty)
60console.log(Object.keys(EmployeeProto.prototype)); // ["work"]
61
62// Instance properties vs prototype properties
63class Config {
64 instances = []; // own property (per instance)
65 getConfig() {} // on prototype (shared)
66}
67
68// Equivalent:
69function ConfigProto() {
70 this.instances = [];
71}
72ConfigProto.prototype.getConfig = function() {};
FeatureClass SyntaxPrototype Syntax
ReadabilityClean, declarativeVerbose, manual
Method enumerableNon-enumerable (correct)Enumerable (unless definedProperty)
ConstructorDedicated constructor methodFunction body is constructor
Call without newThrows TypeErrorPollutes global scope
Static methodsstatic keywordFunction property assignment
Private fields# syntaxNo native support
under-the-hood.js
JavaScript
1// Babel/TypeScript output example
2// Input: class Dog extends Animal { bark() { super.speak(); } }
3// Output (simplified):
4function Dog(name) {
5 Animal.call(this, name);
6}
7Object.setPrototypeOf(Dog.prototype, Animal.prototype);
8Dog.prototype.bark = function bark() {
9 Animal.prototype.speak.call(this);
10};
11
12// Checking the prototype chain
13class A {}
14class B extends A {}
15class C extends B {}
16
17const c = new C();
18console.log(c instanceof C); // true
19console.log(c instanceof B); // true
20console.log(c instanceof A); // true
21console.log(c instanceof Object); // true
22
23// The chain: c → C.prototype → B.prototype → A.prototype → Object.prototype
24
25// constructor property
26console.log(c.constructor === C); // true
27console.log(C.prototype.constructor === C); // true
28
29// __proto__ vs prototype
30console.log(c.__proto__ === C.prototype); // true (deprecated)
31console.log(Object.getPrototypeOf(c) === C.prototype); // true (standard)
Class Best Practices
Prefer composition over inheritance — deep hierarchies are brittle and hard to refactor
Keep classes focused — a class should have one responsibility (Single Responsibility Principle)
Use private fields (#) for true encapsulation instead of _convention
Always call super() in a subclass constructor before accessing this
Use static methods for factory functions, utility methods, and configuration
Avoid deep inheritance chains — max 2-3 levels deep
Use getters/setters for computed properties and validation, not for side effects
Prefer class syntax over manual prototype manipulation for new code
Use instanceof carefully — it checks prototype chains, not types
Document abstract methods clearly since JavaScript lacks native abstract support
Consider using TypeScript for larger projects — it adds interface, abstract, access modifiers, and type safety to classes
🔥

pro tip

The best JavaScript code uses a mix of paradigms. Use classes when you need encapsulation, state, and identity (objects with internal state and methods). Use functions and modules for stateless operations, utilities, and composition. Use factory functions and closures when you need even more flexibility than classes provide.
$Blueprint — Engineering Documentation·Section ID: JS-CLASSES·Revision: 1.0