Classes in JavaScript provide a cleaner syntax for creating objects and implementing inheritance. Here's a comprehensive guide.
Basic Class Syntax#
1// Class declaration
2class Person {
3 constructor(name, age) {
4 this.name = name;
5 this.age = age;
6 }
7
8 greet() {
9 return `Hello, I'm ${this.name}`;
10 }
11
12 getInfo() {
13 return `${this.name}, ${this.age} years old`;
14 }
15}
16
17const john = new Person('John', 30);
18john.greet(); // "Hello, I'm John"
19
20// Class expression
21const Animal = class {
22 constructor(name) {
23 this.name = name;
24 }
25};
26
27// Named class expression
28const Dog = class DogClass {
29 // DogClass only available inside class body
30};Constructor#
1class User {
2 // Constructor runs when using 'new'
3 constructor(name, email) {
4 // Initialize instance properties
5 this.name = name;
6 this.email = email;
7 this.createdAt = new Date();
8
9 // Validation
10 if (!email.includes('@')) {
11 throw new Error('Invalid email');
12 }
13 }
14}
15
16const user = new User('John', 'john@example.com');
17
18// Without new throws error
19// User('John', 'john@example.com'); // TypeError
20
21// Constructor can return object (rare)
22class Override {
23 constructor() {
24 return { custom: true };
25 }
26}
27
28new Override(); // { custom: true }Instance Methods#
1class Calculator {
2 constructor(value = 0) {
3 this.value = value;
4 }
5
6 add(n) {
7 this.value += n;
8 return this; // Enable chaining
9 }
10
11 subtract(n) {
12 this.value -= n;
13 return this;
14 }
15
16 multiply(n) {
17 this.value *= n;
18 return this;
19 }
20
21 getResult() {
22 return this.value;
23 }
24}
25
26const calc = new Calculator(10)
27 .add(5)
28 .multiply(2)
29 .subtract(3)
30 .getResult(); // 27Static Methods and Properties#
1class MathUtils {
2 // Static property
3 static PI = 3.14159;
4
5 // Static method
6 static add(a, b) {
7 return a + b;
8 }
9
10 static multiply(a, b) {
11 return a * b;
12 }
13
14 // Factory method pattern
15 static createRandom() {
16 return Math.random();
17 }
18}
19
20// Called on class, not instance
21MathUtils.add(2, 3); // 5
22MathUtils.PI; // 3.14159
23
24// Not available on instances
25const math = new MathUtils();
26// math.add(2, 3); // Error: add is not a function
27
28// Factory pattern
29class User {
30 constructor(name, role) {
31 this.name = name;
32 this.role = role;
33 }
34
35 static createAdmin(name) {
36 return new User(name, 'admin');
37 }
38
39 static createGuest() {
40 return new User('Guest', 'guest');
41 }
42}
43
44const admin = User.createAdmin('John');
45const guest = User.createGuest();Getters and Setters#
1class Circle {
2 constructor(radius) {
3 this._radius = radius;
4 }
5
6 // Getter
7 get radius() {
8 return this._radius;
9 }
10
11 // Setter with validation
12 set radius(value) {
13 if (value <= 0) {
14 throw new Error('Radius must be positive');
15 }
16 this._radius = value;
17 }
18
19 // Computed property getter
20 get area() {
21 return Math.PI * this._radius ** 2;
22 }
23
24 get circumference() {
25 return 2 * Math.PI * this._radius;
26 }
27}
28
29const circle = new Circle(5);
30circle.radius; // 5
31circle.area; // 78.54...
32circle.radius = 10; // Uses setter
33circle.area; // 314.15...
34
35// Read-only property
36class ReadOnly {
37 constructor(value) {
38 this._value = value;
39 }
40
41 get value() {
42 return this._value;
43 }
44 // No setter - effectively read-only
45}Private Fields#
1class BankAccount {
2 // Private fields (prefix with #)
3 #balance = 0;
4 #pin;
5
6 constructor(initialBalance, pin) {
7 this.#balance = initialBalance;
8 this.#pin = pin;
9 }
10
11 // Public methods access private fields
12 deposit(amount) {
13 if (amount > 0) {
14 this.#balance += amount;
15 }
16 }
17
18 withdraw(amount, pin) {
19 if (pin !== this.#pin) {
20 throw new Error('Invalid PIN');
21 }
22 if (amount > this.#balance) {
23 throw new Error('Insufficient funds');
24 }
25 this.#balance -= amount;
26 return amount;
27 }
28
29 getBalance(pin) {
30 if (pin !== this.#pin) {
31 throw new Error('Invalid PIN');
32 }
33 return this.#balance;
34 }
35
36 // Private method
37 #validateTransaction(amount) {
38 return amount > 0 && amount <= this.#balance;
39 }
40}
41
42const account = new BankAccount(1000, '1234');
43account.deposit(500);
44account.getBalance('1234'); // 1500
45
46// Cannot access private fields
47// account.#balance; // SyntaxError
48// account.#pin; // SyntaxErrorInheritance#
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() {
11 console.log(`${this.name} moves`);
12 }
13}
14
15class Dog extends Animal {
16 constructor(name, breed) {
17 // Must call super() before using 'this'
18 super(name);
19 this.breed = breed;
20 }
21
22 // Override parent method
23 speak() {
24 console.log(`${this.name} barks`);
25 }
26
27 // Call parent method
28 move() {
29 super.move(); // Call Animal.move()
30 console.log(`${this.name} runs`);
31 }
32
33 // Additional method
34 fetch() {
35 console.log(`${this.name} fetches the ball`);
36 }
37}
38
39const dog = new Dog('Rex', 'German Shepherd');
40dog.speak(); // "Rex barks"
41dog.move(); // "Rex moves" then "Rex runs"
42dog.fetch(); // "Rex fetches the ball"
43
44// instanceof checks
45dog instanceof Dog; // true
46dog instanceof Animal; // trueStatic Inheritance#
1class Parent {
2 static version = '1.0';
3
4 static describe() {
5 return 'Parent class';
6 }
7}
8
9class Child extends Parent {
10 static describe() {
11 return `${super.describe()} - Child class`;
12 }
13}
14
15Child.version; // '1.0' (inherited)
16Child.describe(); // 'Parent class - Child class'Abstract-like Patterns#
1// JavaScript doesn't have abstract classes, but we can simulate
2class Shape {
3 constructor() {
4 if (new.target === Shape) {
5 throw new Error('Shape cannot be instantiated directly');
6 }
7 }
8
9 // "Abstract" method
10 getArea() {
11 throw new Error('getArea must be implemented');
12 }
13
14 // Concrete method
15 describe() {
16 return `Shape with area: ${this.getArea()}`;
17 }
18}
19
20class Rectangle extends Shape {
21 constructor(width, height) {
22 super();
23 this.width = width;
24 this.height = height;
25 }
26
27 getArea() {
28 return this.width * this.height;
29 }
30}
31
32// new Shape(); // Error
33const rect = new Rectangle(10, 5);
34rect.getArea(); // 50
35rect.describe(); // "Shape with area: 50"Mixins#
1// Mixin functions
2const Serializable = (Base) => class extends Base {
3 serialize() {
4 return JSON.stringify(this);
5 }
6
7 static deserialize(json) {
8 return Object.assign(new this(), JSON.parse(json));
9 }
10};
11
12const Timestamped = (Base) => class extends Base {
13 constructor(...args) {
14 super(...args);
15 this.createdAt = new Date();
16 this.updatedAt = new Date();
17 }
18
19 touch() {
20 this.updatedAt = new Date();
21 }
22};
23
24// Apply mixins
25class User extends Timestamped(Serializable(Object)) {
26 constructor(name) {
27 super();
28 this.name = name;
29 }
30}
31
32const user = new User('John');
33user.serialize(); // JSON string
34user.createdAt; // Date
35user.touch(); // Updates timestampClass Fields#
1class Counter {
2 // Public field with default
3 count = 0;
4
5 // Private field
6 #maxCount = 100;
7
8 // Arrow function as method (bound to instance)
9 increment = () => {
10 if (this.count < this.#maxCount) {
11 this.count++;
12 }
13 };
14
15 decrement = () => {
16 if (this.count > 0) {
17 this.count--;
18 }
19 };
20}
21
22const counter = new Counter();
23counter.increment();
24
25// Arrow methods maintain 'this' binding
26const { increment } = counter;
27increment(); // Still works!
28counter.count; // 2Symbol Methods#
1class Collection {
2 constructor(items) {
3 this.items = items;
4 }
5
6 // Make iterable
7 [Symbol.iterator]() {
8 let index = 0;
9 const items = this.items;
10 return {
11 next() {
12 if (index < items.length) {
13 return { value: items[index++], done: false };
14 }
15 return { done: true };
16 }
17 };
18 }
19
20 // Custom string representation
21 [Symbol.toStringTag] = 'Collection';
22
23 // Custom primitive conversion
24 [Symbol.toPrimitive](hint) {
25 if (hint === 'number') {
26 return this.items.length;
27 }
28 if (hint === 'string') {
29 return this.items.join(', ');
30 }
31 return this.items;
32 }
33}
34
35const collection = new Collection([1, 2, 3]);
36
37// Iteration
38for (const item of collection) {
39 console.log(item); // 1, 2, 3
40}
41
42// Spread
43[...collection]; // [1, 2, 3]
44
45// String tag
46Object.prototype.toString.call(collection); // "[object Collection]"Checking Class Types#
1class Animal {}
2class Dog extends Animal {}
3
4const dog = new Dog();
5
6// instanceof
7dog instanceof Dog; // true
8dog instanceof Animal; // true
9dog instanceof Object; // true
10
11// constructor
12dog.constructor === Dog; // true
13
14// isPrototypeOf
15Animal.prototype.isPrototypeOf(dog); // true
16
17// Custom instanceof behavior
18class Validator {
19 static [Symbol.hasInstance](obj) {
20 return obj && typeof obj.validate === 'function';
21 }
22}
23
24const obj = { validate() {} };
25obj instanceof Validator; // trueCommon Patterns#
1// Singleton
2class Database {
3 static #instance;
4
5 constructor() {
6 if (Database.#instance) {
7 return Database.#instance;
8 }
9 Database.#instance = this;
10 this.connection = null;
11 }
12
13 connect() {
14 // Connection logic
15 }
16}
17
18const db1 = new Database();
19const db2 = new Database();
20db1 === db2; // true
21
22// Builder pattern
23class QueryBuilder {
24 #query = { select: '*', from: '', where: [] };
25
26 select(fields) {
27 this.#query.select = fields;
28 return this;
29 }
30
31 from(table) {
32 this.#query.from = table;
33 return this;
34 }
35
36 where(condition) {
37 this.#query.where.push(condition);
38 return this;
39 }
40
41 build() {
42 const { select, from, where } = this.#query;
43 let sql = `SELECT ${select} FROM ${from}`;
44 if (where.length) {
45 sql += ` WHERE ${where.join(' AND ')}`;
46 }
47 return sql;
48 }
49}
50
51const query = new QueryBuilder()
52 .select('name, email')
53 .from('users')
54 .where('active = true')
55 .where('age > 18')
56 .build();Best Practices#
Class Design:
✓ Use classes for objects with behavior
✓ Keep constructors simple
✓ Use private fields for encapsulation
✓ Prefer composition over inheritance
Methods:
✓ Use arrow properties for callbacks
✓ Return 'this' for chaining
✓ Use getters for computed properties
✓ Use setters for validation
Inheritance:
✓ Favor shallow hierarchies
✓ Always call super() first
✓ Use mixins for shared behavior
✓ Consider composition instead
Avoid:
✗ Deep inheritance chains
✗ Forgetting 'new' keyword
✗ Exposing internal state
✗ Over-engineering with patterns
Conclusion#
JavaScript classes provide clean syntax for object-oriented programming. Use private fields for encapsulation, getters/setters for controlled access, and static methods for utility functions. Prefer composition and mixins over deep inheritance. Remember that classes are syntactic sugar over prototypes, but they offer better organization and readability.