Back to Blog
JavaScriptClassesOOPES6

JavaScript Classes Guide

Master JavaScript classes including inheritance, static methods, and private fields.

B
Bootspring Team
Engineering
September 9, 2018
8 min read

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(); // 27

Static 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; // SyntaxError

Inheritance#

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; // true

Static 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 timestamp

Class 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; // 2

Symbol 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; // true

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

Share this article

Help spread the word about Bootspring