Object-Oriented Programming (OOP)

Learn the basics of Object-Oriented Programming in JavaScript using classes, constructors, inheritance, methods, and more.

Loading...
Object-Oriented Programming (OOP)

What is Object-Oriented Programming (OOP)?

Object-Oriented Programming is a way of writing code that centers on the idea of objects. These objects can be thought of as building blocks to create things.

In OOP, you create these objects based on a blueprint known as a class.

Think of it like designing cars in a factory:

  • You have a blueprint (called a class).
  • You use that blueprint to create multiple cars (objects).
  • Each car has the same structure, but can have different details (color, speed, brand).

OOP helps you keep your code organized and makes it easier to understand how different parts of your program interact with each other.


Core Principles of OOP

Object-Oriented Programming is based on four main principles:

  • Encapsulation: Keeping data (properties) and functions (methods) together inside a class and restricting direct access to some of the object’s components to protect the data.
  • Inheritance: Allowing a class to inherit properties and methods from another class to promote code reuse.
  • Polymorphism: The ability for different classes to be treated as instances of the same class through a common interface.
  • Abstraction: Hiding complex implementation details and showing only the necessary parts.

Why use OOP?

  • Keeps code clean and organized
  • Makes it easier to reuse logic (no copy-pasting!)
  • Helps you model real-world things in code
  • Makes collaboration and scaling easier
  • Enables encapsulation to protect data and group related functionality together
  • Supports inheritance so you can create new classes based on existing ones without rewriting code

Classes and Constructors

In JavaScript, a class is like a blueprint, it defines what an object should look like and how it should behave.

The constructor is a special function inside the class. It runs automatically when you create a new object and sets up the initial values (just like assembling a new car using a template).

For example:

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

Here,

  • class Car is the blueprint for making car objects.
  • constructor(company, model) is like the instruction manual, it tells JavaScript how to build each car.
  • this.company and this.model are properties assigned to each object (like setting the brand and model of the car).

So whenever you say new Car("Toyota", "Camry"), JavaScript uses the class and constructor to build a brand-new object with those details.


Creating Objects

Once you've defined a class (blueprint), you can build objects (instances) from it using the new keyword.

Example:

const carObject = new Car("Ford", "Ecosport");

Here,

  • new Car(...) tells JavaScript: "Use the Car blueprint and run the constructor."

  • "Ford" and "Ecosport" are passed into the constructor.

  • A brand-new object is created with these values assigned.

  • When you do new Car("Ford", "Ecosport"), it creates an object like:

    {
    company: "Ford",
    model: "Ecosport"
    }

Now you have a carObject that holds details about a specific car, ready to use in your program.


Prototype

In JavaScript, prototype is how objects share methods and properties.

When you create a new object using a class or constructor function, JavaScript automatically links it to its prototype. This means your object can access any method or property defined on that prototype, even if the object doesn’t define it directly.

When you try to access a property or method on an object, JavaScript first checks if that property or method exists on the object itself. If it doesn't, JavaScript looks up the prototype object. If the prototype doesn't have it, JavaScript keeps looking at "parent" prototypes until it finds the property or method.

To put it simply, think of it as borrowing things from a friend. If you don't have something, you ask your friend. If your friend doesn't have it, they might ask their friend, and this can continue until someone has what you're looking for.

For example:

class Car {
  constructor(company, model) {
    this.company = company;
    this.model = model;
  }
}
 
// Add a method to the Car prototype
Car.prototype.hello = function () {
  console.log(`Hello, I'm ${this.model} model of ${this.company} company!`);
};
 
// Create car objects
const carObject1 = new Car("Ford", "Ecosport");
const carObject2 = new Car("Toyota", "Fortuner");
 
carObject1.hello(); // Output: Hello, I'm Ecosport model of Ford company!
carObject2.hello(); // Output: Hello, I'm Fortuner model of Toyota company!

Here,

  • class Car { ... }: This defines a blueprint for creating car objects.
  • constructor(company, model): A special method that runs when you create a new object using new Car(...).
  • this.company = company: Sets the company property on the new object.
  • this.model = model: Sets the model property on the new object.
  • Car.prototype.hello = function () {...};:
    • Adds a method hello() to the Car prototype.
    • All objects created using new Car(...) can access this method via the prototype chain.
    • Inside the function, this.model and this.company refer to the properties of the object calling the method.
  • const carObject1 = new Car("Ford", "Ecosport"); carObject1 is an object representing a Ford Ecosport.
  • const carObject1 = new Car("Toyota", "Fortuner"); carObject1 is an object representing a Toyota Fortuner.
  • Both are instances of the Car class.
  • When you call carObject1.hello(), JavaScript looks for hello() on carObject1 and doesn’t find it directly. So it looks at Car.prototype, finds hello, and runs it with this pointing to carObject1.
  • Same for carObject2.

When you define methods inside a class, they are automatically added to the class's prototype behind the scenes.

For example:

// class and constructor
class Car {
  constructor(company, model) {
    this.company = company;
    this.model = model;
  }
  hello() {
    // Add a method to the Car prototype
    console.log(`Hello, I'm ${this.model} model of ${this.company} company!`);
  }
}
 
// Creating objects using the Car constructor
const carObject1 = new Car("Ford", "Ecosport");
const carObject2 = new Car("Toyota", "Fortuner");
 
carObject1.hello(); // Output: Hello, I'm Ecosport model of Ford company!
carObject2.hello(); // Output: Hello, I'm Fortuner model of Toyota company!

This is just a cleaner syntax. Internally, it’s doing the same thing as Car.prototype.hello = ....

Note: While you can add methods directly to a class’s prototype (like Car.prototype.hello = ...), the modern and preferred way is to define methods inside the class body. This keeps your code cleaner and easier to read.


this keyword

In JavaScript, this refers to the object that's currently executing the code.

Inside a class, when you write this.name, you're referring to the name property of that specific object (instance).

Learn more about this keyword!

class Person {
  constructor(name) {
    this.name = name;
  }
 
  hello() {
    console.log(`Hello, I'm ${this.name}!`);
  }
}
 
const person1 = new Person("John");
const person2 = new Person("Mark");
 
person1.hello(); // Output: Hello, I'm John!
person2.hello(); // Output: Hello, I'm Mark!

Here,

  • constructor(name): Sets this.name to the value passed when creating a new Person.
  • hello(): A method that prints this.name, referring to the object calling the method.
  • Creating Instances:
    const person1 = new Person("John");
    const person2 = new Person("Mark");
  • You now have:
    person1 = { name: "John", hello: [Function] };
    person2 = { name: "Mark", hello: [Function] };
  • Each Person has its own name, but shares the same hello() method.
  • person1.hello() refers to person1, so it prints "Hello, I'm John!".
  • person2.hello() refers to person2, so it prints "Hello, I'm Mark!".

Without this, you’d have to hardcode or duplicate logic per object. this makes methods reusable and object-specific.


Class Inheritance

Inheritance is a key feature of OOP that lets a new class (child) inherit properties and methods from an existing class (parent). This helps you reuse code and build on existing functionality without rewriting it.

For example, you can create a base Car class and then extend it to make a CarYear class that includes a year property.

// Parent class
class Car {
  constructor(company, model) {
    this.company = company;
    this.model = model;
  }
}
 
// Child class
class CarYear extends Car {
  constructor(company, model, year) {
    // Call parent constructor with company and model
    super(company, model);
    // Add new property specific to CarYear
    this.year = year;
  }
}
 
// Create a new car
const carObject = new CarYear("Ford", "Ecosport", 2013);
 
console.log(carObject.company); // Output: Ford
console.log(carObject.model); // Output: Ecosport
console.log(carObject.year); // Output: 2013

Here,

  • Car is the parent class. It defines company and model.
  • CarYear is the child class. It inherits from Car using extends.
  • Inside CarYear, you call super(company, model) to run the Car constructor.
  • Then you add an extra year property specific to CarYear.

What is Super?

The super keyword is used to call and access methods or properties from a parent class within a child class.

You can also call methods from the parent using super.methodName().


Method Overriding

Method overriding means redefining a method in a child class that already exists in the parent class.

The method name stays the same, but its behavior changes in the child.

This is useful when the child class needs a more specific or updated version of the parent’s method.

For example:

// Parent class
class Car {
  constructor(company, model) {
    this.company = company;
    this.model = model;
  }
 
  hello() {
    console.log(`Hello, I'm ${this.model} model of ${this.company} company!`);
  }
}
 
// Child class
class CarYear extends Car {
  constructor(company, model, year) {
    super(company, model);
    this.year = year;
  }
 
  // Overriding the hello method
  hello() {
    console.log(
      `Hello, I'm ${this.year} ${this.model} model of ${this.company} company!`
    );
  }
}
 
// Creating object using the Car constructor
const carObject = new Car("Ford", "Ecosport");
 
// Creating object using the CarYear constructor
const CarYearObject = new CarYear("Ford", "Ecosport", 2013);
 
carObject.hello(); // Output: Hello, I'm Ecosport model of Ford company!
CarYearObject.hello(); // Output: Hello, I'm 2013 Ecosport model of Ford company!

Here,

  • You have a Car class with a method hello() and a CarYear class that extends Car.
  • In the CarYear class, you override the hello() method.
  • When you create an object of the CarYear class and call the hello() method, it uses the overridden method from the CarYear class, not the one from the Car class.
  • This is method overriding.

Note: If needed, the child class can still access the parent method using super.hello().

class Car {
  constructor(company, model) {
    this.company = company;
    this.model = model;
  }
 
  hello() {
    console.log(`Hello from Car: ${this.model} by ${this.company}`);
  }
}
 
class CarYear extends Car {
  constructor(company, model, year) {
    super(company, model);
    this.year = year;
  }
 
  hello() {
    // Call parent's version first
    super.hello();
 
    // Add extra behavior
    console.log(`This model was launched in ${this.year}.`);
  }
}
 
const myCar = new CarYear("Toyota", "Fortuner", 2022);
myCar.hello();
 
// Output:
 
// Hello from Car: Fortuner by Toyota
// This model was launched in 2022.

Static Methods

In JavaScript, static methods belong to the class itself, not to the objects created from it.

That means:

  • You can’t call a static method on an object (like carObject.info()).
  • You must call it on the class (like Car.info()).

Static methods are defined using the static keyword within a class declaration.

class Car {
  constructor(company, model) {
    this.company = company;
    this.model = model;
  }
 
  // Instance method — works on object
  start() {
    console.log(`${this.model} of ${this.company}  is starting...`);
  }
 
  // Static method — works on class
  static info() {
    console.log("This is a Car class.");
  }
}
 
// Create an object
const carObject = new Car("Ford", "Ecosport");
 
// Call instance method
carObject.start(); // Ecosport of Ford is starting...
 
// Call static method
Car.info(); // This is a Car class.
 
// carObject.info();   // Error: carObject.info is not a function

When to Use Static vs Instance Methods

  • Instance methods (like start()) work on individual objects and can access instance properties via this.
  • Static methods (like info()) belong to the class itself and cannot access instance properties. Use them for utility functions or actions that don’t depend on a specific object.

For example: Car.info() can show general info about the class, while carObject.start() controls a specific car.


Getters and Setters

In JavaScript classes, getters and setters let you control how properties are accessed and updated.

  • get lets you access a property
  • set lets you update a property with custom logic or validation

For example:

class Car {
  constructor(company, model) {
    this._company = company; // convention: underscore means "private"
    this._model = model;
  }
 
  // Getter for the company property
  get company() {
    return this._company;
  }
 
  // Setter for the company property
  set company(newCompany) {
    if (typeof newCompany === "string") {
      this._company = newCompany;
    } else {
      console.log("company must be a string");
    }
  }
 
  // Getter for the model property
  get model() {
    return this._model;
  }
 
  // Setter for the model property
  set model(newModel) {
    if (typeof newModel === "string") {
      this._model = newModel;
    } else {
      console.log("model must be a string");
    }
  }
}
 
// Create an object
const carObject = new Car("Ford", "Ecosport");
 
// Access properties like normal variables
console.log(carObject.company); // Ford
console.log(carObject.model); // Ecosport
 
// Update using setters
carObject.company = "Toyota";
carObject.model = "Fortuner";
 
console.log(carObject.company); // Toyota
console.log(carObject.model); // Fortuner
 
// Try setting invalid data
carObject.model = 123; // model must be a string

Here,

  • The Car class has getter and setter methods for the company and model properties.
  • The getters allow you to retrieve the values of these properties, while the setters provide a way to validate and set new values for the properties.
  • The underscores _company and _model are used to indicate that these properties are meant to be private to the Car class and should not be accessed or modified directly from outside the class.

Private Fields

In modern JavaScript, you can create truly private properties in a class using the # prefix. These private fields can only be accessed within the class and are not accessible outside.

Example:

class Car {
  #engineNumber; // private field
 
  constructor(company, model, engineNumber) {
    this.company = company;
    this.model = model;
    this.#engineNumber = engineNumber; // initialize private field
  }
 
  getEngineNumber() {
    return this.#engineNumber;
  }
}
 
const myCar = new Car("Tesla", "Model S", "ENG12345");
 
console.log(myCar.company); // Tesla
console.log(myCar.getEngineNumber()); // ENG12345
console.log(myCar.#engineNumber); // SyntaxError: Private field '#engineNumber' must be declared in an enclosing class

Private fields help with encapsulation by hiding sensitive data inside the class.


instanceof Operator

The instanceof operator is used to check if an object was created from a specific class (or constructor function).

For example:

// Parent class
class Car {
  constructor(company, model) {
    this.company = company;
    this.model = model;
  }
}
 
// Child class
class CarYear extends Car {
  constructor(company, model, year) {
    super(company, model);
    this.year = year;
  }
}
 
// Creating object using the Car constructor
const carObject = new Car("Ford", "Ecosport");
 
// Creating object using the CarYear constructor
const CarYearObject = new CarYear("Ford", "Ecosport", 2013);
 
// Using instanceof operator
 
console.log(carObject instanceof Car); // Output: true
console.log(carObject instanceof CarYear); // Output: false
 
console.log(CarYearObject instanceof Car); // Output: true
console.log(CarYearObject instanceof CarYear); // Output: true

Here,

  • You have a Car class and a CarYear class that inherits from Car.
  • You create instances(objects) of both classes and then use the instanceof operator.
  • carObject is an instance of Car, but it's not an instance of CarYearObject.
  • CarYearObject is an instance of both Car and CarYear.

Support my work!