Object-Oriented Programming in JavaScript

Master ES6 classes, inheritance, and modern OOP patterns to build cleaner, more maintainable web applications.

Object-oriented programming has become a cornerstone of modern JavaScript development. Whether you're building a complex React application, creating reusable UI components, or architecting a large-scale web application, understanding OOP principles in JavaScript helps you write cleaner, more maintainable code. This guide explores how JavaScript implements object-oriented concepts and shows you practical ways to apply them in your projects.

Topics covered:

  • ES6 class syntax and modern features
  • Inheritance and the prototype chain
  • Encapsulation with private fields
  • Polymorphism and method overriding
  • Performance best practices

Is JavaScript an Object-Oriented Language?

This question often surprises developers new to JavaScript. The answer requires understanding what we mean by "object-oriented" and how JavaScript's unique prototype-based system differs from class-based languages like Java or C++. JavaScript is fundamentally an object-oriented language, but it takes a different approach than many traditional OOP languages.

Key differences from class-based languages:

  • Instead of classes as blueprinted templates, JavaScript uses prototypes--objects that serve as templates for creating other objects
  • Every object can inherit properties and methods from another object through the prototype chain
  • ES6 classes provide syntactic sugar that makes object-oriented patterns more accessible

MDN Web Docs - Using classes

The Four Pillars of OOP in JavaScript

PillarDescriptionJavaScript Implementation
EncapsulationBundling data and behavior, controlling accessPrivate fields, closures, modules
InheritanceReusing code through class hierarchiesPrototype chain, extends keyword
PolymorphismSame interface, different implementationsMethod overriding
AbstractionHiding complexity, exposing essentialsModule patterns, factory functions

Understanding how JavaScript handles OOP concepts helps when working with modern web APIs and building complex applications. The prototype-based system provides flexibility that class-based languages lack, enabling powerful patterns like composition over classical inheritance.

ES6 Classes: Modern JavaScript OOP Syntax

The ES6 class syntax revolutionized how JavaScript developers write object-oriented code. While classes existed conceptually in JavaScript before ES6--through constructor functions and prototypes--the class keyword provided a cleaner, more intuitive syntax that aligned JavaScript with other popular programming languages.

Class Declarations and Expressions

Class declarations are the most common way to define classes in JavaScript. A class declaration begins with the class keyword followed by the class name and a code block containing the class body. The class body contains methods, constructor definitions, and field declarations that define what the class can do and what data it holds.

class Car {
 constructor(brand, model) {
 this.brand = brand;
 this.model = model;
 }

 drive() {
 console.log(`${this.brand} ${this.model} is driving...`);
 }
}

const myCar = new Car('Toyota', 'Camry');
myCar.drive(); // Toyota Camry is driving...

Class expressions provide an alternative syntax where the class is assigned to a variable. This is useful when classes need to be passed as arguments, returned from functions, or defined conditionally. Class expressions can be named or anonymous, and the name is only visible within the class body itself.

const Vehicle = class {
 constructor(type) {
 this.type = type;
 }

 start() {
 console.log(`${this.type} is starting...`);
 }

 stop() {
 console.log(`${this.type} is stopping...`);
 }
};

const motorcycle = new Vehicle('Motorcycle');
motorcycle.start(); // Motorcycle is starting...

Understanding the difference between class declarations and expressions is important for writing idiomatic JavaScript. Class declarations are not hoisted like function declarations--meaning you cannot use a class before it appears in your code. This behavior is similar to let and const variables and helps prevent errors from using classes before they're defined.

Constructor Methods

The constructor method is a special method for creating and initializing objects. There can only be one constructor per class. When you create a new instance with new, the constructor is automatically called.

Constructor best practices:

  • Initialize all instance properties
  • Validate inputs and throw errors early
  • Use default parameters for optional values

Constructors are essential when building custom web applications that require proper object initialization and state management.

Constructor Example
1class User {2 constructor(name, email, role) {3 this.name = name;4 this.email = email;5 this.role = role;6 this.lastLogin = null;7 this.loginCount = 0;8 }9 10 login() {11 this.lastLogin = new Date();12 this.loginCount++;13 console.log(`${this.name} logged in`);14 }15}16 17const user = new User('John', '[email protected]', 'admin');
Instance Methods with Method Chaining
1class Calculator {2 constructor(initialValue = 0) {3 this.value = initialValue;4 }5 6 add(number) {7 this.value += number;8 return this; // Enable chaining9 }10 11 subtract(number) {12 this.value -= number;13 return this;14 }15 16 getValue() {17 return this.value;18 }19}20 21// Usage with method chaining22const result = new Calculator(10)23 .add(5)24 .subtract(3)25 .add(2)26 .getValue(); // 14

Instance Methods

Instance methods define the behaviors that objects can perform. These methods have access to object properties through this and can modify state, perform calculations, or interact with other systems.

Method patterns:

  • Regular methods for actions and operations
  • Getter methods (get) for computed properties
  • Setter methods (set) for controlled property assignment
  • Async methods for asynchronous operations
  • Generator methods (*) for iterables

When working with animations in JavaScript, instance methods are commonly used to control timing, state transitions, and event handling. Understanding how to properly structure methods is essential for creating smooth, interactive web experiences.

Advanced Class Features

Modern JavaScript has expanded class capabilities with features that address common requirements in large-scale web applications.

Static Properties and Methods

Static members belong to the class itself, not instances. They're accessed directly on the class and are useful for utility functions, constants, or factory methods.

class MathUtils {
 static PI = 3.14159;

 static calculateCircleArea(radius) {
 return this.PI * radius * radius;
 }

 static createRandom(min, max) {
 return Math.floor(Math.random() * (max - min + 1)) + min;
 }
}

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.calculateCircleArea(5)); // ~78.54

Private Fields for True Encapsulation

Private fields (prefixed with #) provide true encapsulation by making properties inaccessible from outside the class. This is enforced by the JavaScript runtime.

class BankAccount {
 #balance;
 #accountNumber;

 constructor(accountNumber, initialBalance = 0) {
 this.#accountNumber = accountNumber;
 this.#balance = initialBalance;
 }

 deposit(amount) {
 if (amount <= 0) throw new Error('Amount must be positive');
 this.#balance += amount;
 }

 getBalance() {
 return this.#balance;
 }
}

Private fields are particularly useful when building secure Node.js backends where sensitive data needs proper encapsulation. They prevent accidental access to internal state and make your intent explicit to other developers working on the codebase.

MDN Web Docs - Using classes

Inheritance and the Prototype Chain

Inheritance allows classes to build upon other classes, reusing code and establishing hierarchical relationships. JavaScript's inheritance model is based on prototypes, which gives it unique characteristics compared to class-based languages.

The Extends Keyword

The extends keyword establishes inheritance between classes. Subclasses inherit all non-private properties and methods from their superclass.

class Animal {
 constructor(name) {
 this.name = name;
 }

 speak() {
 console.log(`${this.name} makes a sound`);
 }
}

class Dog extends Animal {
 constructor(name, breed) {
 super(name); // Call parent constructor
 this.breed = breed;
 }

 speak() {
 console.log(`${this.name} barks`);
 }
}

const dog = new Dog('Rex', 'German Shepherd');
dog.speak(); // Rex barks (overridden method)

Method Overriding and Polymorphism

Method overriding allows subclasses to provide specific implementations. This enables polymorphism, where objects of different types respond to the same method calls differently.

How Prototypes Work

Every JavaScript object has an internal link to its prototype. When accessing a property, JavaScript searches the object, then its prototype, up the chain until reaching Object.prototype. Class methods are stored on the prototype and shared across all instances--memory efficient!

Understanding the prototype chain is crucial when working with web components and other advanced JavaScript patterns where inheritance plays a key role in component architecture.

MDN Web Docs - Using classes

Performance Considerations

Understanding class performance helps you make informed decisions about when and how to use OOP patterns in your JavaScript applications.

Memory Efficiency

Class methods defined in the class body are stored on the prototype and shared across all instances. This is memory-efficient--1000 instances share 1 set of method definitions.

Performance Best Practices

PracticeDescription
Share methods via prototypeAvoid defining methods in constructor
Use private fields sparinglyThey have slight overhead
Avoid deep inheritance chainsCan impact property access speed
Prefer composition for flexibilityOften more performant than deep hierarchies
Use new with classesThrows error if called without new

Common Pitfalls to Avoid

  • Excessive prototype chains: Each property access traverses the chain
  • Deep inheritance: Harder to maintain, impacts performance
  • Mutating shared state: Static properties are shared across instances
  • Missing super() calls: Causes ReferenceError in constructors

When building high-performance animations or interactive UI components, following these practices ensures smooth user experiences even with complex class hierarchies.

Best Practices for Object-Oriented JavaScript

Keep Classes Focused

Each class should have a single responsibility. When explaining what a class does requires "and" statements, it probably needs splitting. This aligns with the single responsibility principle that makes code easier to test and maintain.

Use Private Fields

Prefer #privateField over _privateField naming conventions. True encapsulation is enforced by the language, making code more robust and your intent clear to other developers.

Composition Over Inheritance

Build complex behaviors by combining smaller objects rather than deep inheritance hierarchies:

// Instead of inheritance chains:
class FlyingAnimal extends Bird { }
class SwimmingAnimal extends Bird { }

// Use composition:
class Flying { fly() { console.log('Flying'); } }
class Swimming { swim() { console.log('Swimming'); } }

class Duck {
 constructor() {
 this.flyBehavior = new Flying();
 this.swimBehavior = new Swimming();
 }
}

Additional Recommendations

  • Use TypeScript for better type documentation
  • Test classes through their public API
  • Keep constructor logic minimal
  • Document expected method behavior

Following these patterns leads to cleaner, more maintainable codebases that scale well as your web application development projects grow in complexity.

Dev.to - OOP JavaScript Guide

Real-World Examples

Component Classes for UI Development

Building reusable UI components is a natural fit for OOP. Components encapsulate state, behavior, and rendering logic--perfect for interactive web applications.

class Carousel {
 #items;
 #currentIndex;

 constructor(items = []) {
 this.#items = items;
 this.#currentIndex = 0;
 }

 getCurrentItem() {
 return this.#items[this.#currentIndex];
 }

 next() {
 this.#currentIndex = (this.#currentIndex + 1) % this.#items.length;
 return this.getCurrentItem();
 }

 prev() {
 this.#currentIndex = (this.#currentIndex - 1 + this.#items.length) % this.#items.length;
 }
}

Service Classes for Business Logic

Service classes encapsulate business logic separate from UI concerns, handling validation, API calls, and data transformation. This separation of concerns makes business logic easier to test and maintain.

class OrderService {
 #apiClient;
 #validator;

 constructor(apiClient, validator) {
 this.#apiClient = apiClient;
 this.#validator = validator;
 }

 async createOrder(orderData) {
 const error = this.#validator.validate(orderData);
 if (error) throw new ValidationError(error);
 return this.#apiClient.post('/orders', orderData);
 }
}

These patterns are especially valuable when building server-side applications with Node.js, where proper separation of concerns and encapsulation lead to more maintainable, testable code.

Dev.to - OOP JavaScript Guide

Frequently Asked Questions

What is the difference between classes and prototypes in JavaScript?

Classes are syntactic sugar over JavaScript's prototype-based system. When you use the class keyword, JavaScript still creates objects with prototypes under the hood. Classes provide a cleaner, more familiar syntax while maintaining the flexibility of prototypal inheritance.

When should I use classes instead of factory functions?

Use classes when you need classical inheritance (extends keyword), when you want private fields (#field syntax), when working with frameworks that expect classes (React class components), or when your team is more familiar with class-based OOP patterns.

How does private fields (#) differ from underscore prefixes (_)?

Private fields (#) are enforced by the JavaScript runtime--code outside the class literally cannot access them. Underscore prefixes (_) are merely a naming convention that signals "intended to be private" but doesn't actually prevent access.

Should I use inheritance or composition?

Prefer composition for most cases. Building objects by combining focused components is more flexible and maintainable than deep inheritance hierarchies. Use inheritance when you have a clear "is-a" relationship and need polymorphic behavior.

Do classes have performance implications?

Class methods are stored on the prototype and shared across instances, which is memory-efficient. The main performance consideration is avoiding deep prototype chains and using private fields only when needed, as they have slight overhead.

Ready to Build Better JavaScript Applications?

Our team specializes in modern JavaScript development, including object-oriented patterns, performance optimization, and scalable architecture.