Object-Oriented Programming in JavaScript: Learn the Fundamentals

Table of Contents

  • Confusing Points
  • Conclusion
  • Introduction

    If you’ve read my Basic JavaScript blog, you already know that I’m documenting my learning journey while jotting down key concepts for quick revision and future reference. My goal is to make these notes useful not just for myself but for others as well, which is why I’m sharing them here.

    Having learned Object-Oriented Programming (OOP) concepts in Java and C++, I was already familiar with the class keyword in JavaScript. However, this is the first time I’ve explored JavaScript OOP in depth and truly understood how things work under the hood. It’s fascinating to see how JavaScript’s implementation of OOP is unique compared to languages like Java and C++.

    I thoroughly enjoyed learning these concepts and uncovering some fun facts along the way. I hope you find them as interesting as I did. Happy reading!

    Object-Oriented Programming (OOP) in JavaScript

    JavaScript is a prototype-based procedural language that supports both Object-Oriented Programming (OOP) and Functional Programming paradigms. Everything defined in JavaScript is an object that extends functionality from a prototype object, making it a prototype-based language.

    Even though JavaScript doesn’t have native classes, it supports the class keyword. However, the class keyword is simply syntactic sugar over prototype-based inheritance.

    OOP is a programming paradigm that focuses on grouping data (properties) and methods (actions) together inside an entity called an object. It mirrors the patterns of real-world objects.

    Object-Oriented Programming vs Prototype-Based Programming

    Object-Oriented Programming (OOP)

    • Data (properties) and actions (methods) are grouped together inside classes.
    • Classes act as blueprints, and objects are created as instances of these classes.
    • Objects are created based on classes, which define the structure and behavior of objects.
    • Inheritance is class-based; a class can extend another class to inherit its properties and methods.
    • Objects are created by instantiating a class using keywords like new or constructors.
    • The structure is rigidly defined by classes, making it less flexible. Modifications require changes in the class definition.
    • Example: A class Car defines a blueprint, and objects like car1 and car2 are instances of that class.

    Prototype-Based Programming (PBP)

    • Objects are derived from existing objects, using prototypes as a base for inheritance.
    • There are no distinct classes; objects inherit directly from other objects.
    • Objects are created by cloning or extending existing objects, known as prototypes.
    • Inheritance is prototype-based; an object inherits directly from another object (the prototype).
    • Objects are created by copying or linking to an existing prototype object.
    • The structure is more flexible because objects can be modified directly, and new properties or methods can be added at runtime.
    • Example: An object CarPrototype is created first, and other objects are derived directly from it, inheriting its properties and methods.

    Javascript OOP behaviours

    Inheritence

    Inheritance allows characteristics (properties and methods) to be passed from a parent to a child, enabling code reuse and extension. In JavaScript, inheritance is implemented through objects. Using Prototypal Inheritance, objects inherit directly from other objects. This creates a prototype chain, where each object can access properties and methods defined in its prototype.

    Polymorphism

    Polymorphism is an concept in OOP that enables us to use the same function in different forms, which reduces repetition and makes the code snippet useful in many different cases. In JavaScript it is implemented by

    1. Generic
    2. Overloading
    3. Structural sub-typing

    Generic (Parametric Polymorphism)

    Generic overloading allows the same function or method to handle different data types or different numbers of arguments.

    const concatData = (x, y) => {
      return y.concat(x);
    };
    console.log(concatData([1, 2], [3, 4])); //[3, 4, 1, 2]
    console.log(concatData("John", "Smith")); //'SmithJohn'
    

    Overloading (Ad-hoc Polymorphism)

    Functions created using ad-hoc polymorphism exhibit different behaviors based on different types of input values. This allows a function to perform different actions depending on the arguments passed to it, enabling greater flexibility in handling various use cases.

    //when used on integers and floats, exhibit the property of addition
    console.log(4 + 5); //9
    
    //when used with strings and array does concatenation.
    console.log("My name is: " + "John Smith"); //My name is John Smith.
    

    Structural Subtyping (Structural Polymorphism / Duck Typing)

    Structural subtyping is a type system paradigm where type compatibility is determined by the structure of types (i.e., the properties and methods an object has) rather than explicit declarations or inheritance. It is informally known as duck typing: “If it looks like a duck and quacks like a duck, it’s treated as a duck.”

    In programming, this means that if an object has the properties and methods expected by some code, it can be used in that context, regardless of its actual type or inheritance.

    JavaScript inherently supports structural subtyping because it is a dynamically typed language. Objects in JavaScript are defined by their properties and methods, not by their declared types or inheritance chains.

    function printDetails(entity) {
      if (entity.name && entity.age) {
        console.log(`Name: ${entity.name}, Age: ${entity.age}`);
      } else {
        console.log("Invalid structure");
      }
    }
    
    const person = { name: "Alice", age: 25 };
    const dog = { name: "Buddy", age: 5, breed: "Golden Retriever" };
    
    printDetails(person); // Output: Name: Alice, Age: 25
    printDetails(dog); // Output: Name: Buddy, Age: 5
    
    /*
    The printDetails function only cares if the object has name and age properties. Any object with this structure is compatible.
    
    The dog object works, even though it has additional properties like breed.
    */
    

    Relation Between Objects

    JavaScript objects support Association, Aggregation, and Composition.

    Association

    When two objects are unrelated and independent of each other, we define a relationship between them independent of any hierarchy. This means neither object is the child nor the parent of the other; the relation is called association.

    For example: In the case of a patient visiting a doctor, there is an independent relationship. The doctor might see multiple patients, and the patient can visit multiple doctors. None of them is a parent or a child. Hence, the relationship is built through association.

    Aggregation

    In aggregation, there is an ownership relationship. It holds a parent/child relation, but an essential property is that the parent can live without the child object and vice versa. The child can also exist independently.

    For example: If an employee works for more than one department in a company, and one department gets deleted, the employee (the child) can still exist within the company. Hence, this relationship exhibits aggregation.

    Composition

    Composition is a special type of aggregation. In this relationship, if the parent object dies, the child object also ceases to exist. More precisely, the parent can exist without the child, but the child cannot exist without its parent object.

    For example: If a department is deleted from a company, all employees in that department (the child objects) are also removed. The department (the parent) controls the existence of the child objects.

    Instantiation

    Instantiation refers to the process of creating an instance of an object based on a class or function constructor. In JavaScript, instantiation typically involves using a constructor function or class to create an object. When you instantiate an object, you are creating a new instance that inherits the properties and methods defined by the constructor or class.

    Functional Instantiation

    Functional instantiation uses a constructor function to create objects. A constructor function is a special type of function that is used with the new keyword to create a new object.

    function Animal(name) {
      this.name = name;
    }
    
    const dog = new Animal("Buddy");
    console.log(dog.name); // "Buddy"
    

    In this approach, the constructor function Animal is used to instantiate a new object (dog).

    Prototypal Instantiation

    Prototypal instantiation involves creating a new object directly from an existing object, which serves as a prototype. This is often done using Object.create().

    const animal = {
      speak: function () {
        console.log("Hello");
      },
    };
    
    const dog = Object.create(animal);
    dog.speak(); // "Hello"
    

    Here, the dog object is created from the animal prototype, and it inherits the speak method.

    Pseudoclassical Instantiation

    Pseudoclassical instantiation combines constructor functions with prototype inheritance, which mimics class-based OOP (Object-Oriented Programming) concepts. This pattern uses the new keyword to instantiate an object, but it also connects the object to a prototype chain.

    function Animal(name) {
      this.name = name;
    }
    
    Animal.prototype.speak = function () {
      console.log(`Hello I'm ${this.name}`);
    };
    
    const dog = new Animal("Buddy");
    dog.speak(); // "Hello I'm Buddy"
    

    In this case, Animal is a constructor function, and dog is created using the new keyword. The speak method is inherited from Animal.prototype.

    Class Instantiation

    In ES6, JavaScript introduced classes, which provide a cleaner and more modern syntax for object creation. Classes work similarly to pseudoclassical instantiation but with a more structured approach.

    class Animal {
      constructor(name) {
        this.name = name;
      }
    
      speak() {
        console.log(`Hello I'm ${this.name}`);
      }
    }
    
    const dog = new Animal("Buddy");
    dog.speak(); // "Hello I'm Buddy"
    

    The Animal class is used to instantiate dog, and it has the speak method defined inside the class.

    Singleton Instantiation

    In the singleton pattern, only one instance of a class or object is created. This ensures that every time the object is accessed, it returns the same instance.

    const Singleton = (function () {
      let instance;
    
      function createInstance() {
        return { name: "Singleton Instance" };
      }
    
      return {
        getInstance: function () {
          if (!instance) {
            instance = createInstance();
          }
          return instance;
        },
      };
    })();
    
    const obj1 = Singleton.getInstance();
    const obj2 = Singleton.getInstance();
    
    console.log(obj1 === obj2); // true, both are the same instance
    

    Prototype

    Every object in JavaSCript has a built-in property, which is called its prototype. The prototype is itself an object, so the prototype will have its own prototype, making what’s called a prototype chain. The chian end when we reach a prototype that has null for its own prototype. When you try to access a property of an object: if the property can’t be found in the object itself, the prototype is searched for the property. If the property still can’t be found, then the prototype’s prototype is searched, and so on until either the property is found or the end of the chain reaached, in which case undefiend is returned.

    Prototypal Inheritence

    JavaScript implements inheritance through a mechanism called prototypal inheritance, where every object has an internal link to another object called its prototype. This forms a prototype chain that ends with null, which has no prototype and serves as the final link in the chain.

    When you try to access a property on an object, JavaScript first looks for it on the object itself. If the property is not found, it continues searching up the prototype chain, checking the object’s prototype and so on, until it either finds the property or reaches the end of the chain. This allows objects to inherit properties and methods from their prototypes dynamically, enabling code reuse and extension.

    Example:

    const proto1 = {
      a: 1,
      b: 2,
    };
    const proto2 = {
      c: 3,
      d: 4,
      __proto__: proto1,
    };
    const proto3 = {
      d: 5,
      e: 6,
      __proto__: proto2,
    };
    console.log(proto3.a);
    // proto3 --> proto2 --> proto1 --> Object.prototype --> null
    

    Class

    • Classes are templates for creating objects. They encapsulate data along with the methods that operate on that data.
    • In JavaScript, classes are built on prototypes and are essentially “special functions”.
    • Classes can be defined in two ways:
      1. Class Declaration:
        class MyClass {
          constructor(name) {
         this.name = name;
          }
          greet() {
         return `Hello, ${this.name}!`;
          }
        }
        
      2. **Class Expressions:**
      ```js
      const MyClass = class{
        constructor(name) {
            this.name = name;
        }
        greet() {
            return `Hello, ${this.name}!`;
        }
    };
    

    1. Constructor

    • The constructor is a special method for creating and initializing objects created within a class.
    • It is automatically called when a new object instance is created using the new keyword.
    • A class can have only one constructor method. If a class attempts to define multiple constructors, it throws a SyntaxError.
    class Person {
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
    }
    
    const person = new Person("John", 30);
    console.log(person.name); // Output: John
    console.log(person.age); // Output: 30
    

    2. Static Methods

    • Static methods are methods that belong to the class itself rather than to any object instance.
    • They are defined using the static keyword and can be called directly on the class.
    • These methods cannot be called on class instances and are often used as utility functions.
    class MathUtils {
      static add(a, b) {
        return a + b;
      }
    }
    
    console.log(MathUtils.add(3, 7)); // Output: 10
    

    3. Private Properties

    • Private properties are declared using the # prefix and are only accessible within the class where they are defined.
    • They help in encapsulation and protect the internal state of the object from being accessed or modified directly.
    class BankAccount {
      #balance = 0; // Private property
    
      deposit(amount) {
        this.#balance += amount;
      }
    
      getBalance() {
        return this.#balance;
      }
    }
    
    const account = new BankAccount();
    account.deposit(100);
    console.log(account.getBalance()); // Output: 100
    

    4. Inheritance

    • Inheritance is a mechanism for a class to inherit properties and methods from another class.
    • It is implemented using the extends keyword to create a child class from a parent class.
    • The super keyword is used to call the constructor or other methods of the parent class.
    class Animal {
      constructor(name) {
        this.name = name;
      }
      makeSound() {
        console.log("Animal sound");
      }
    }
    
    class Dog extends Animal {
      constructor(name) {
        super(name);
      }
      makeSound() {
        console.log("Woof! Woof!");
      }
    }
    
    const dog = new Dog("Buddy");
    dog.makeSound(); // Output: Woof! Woof!
    

    Confusing Points

    The following are concepts where I had doubts and needed clarification.

    Difference Between [[Prototype]] and prototype

    [[Prototype]]

    • [[Prototype]] is an internal property of an object that links it to another object, known as its prototype.
    • This link forms the foundation of JavaScript’s inheritance system, allowing objects to share properties and methods via the prototype chain.
    • While you cannot directly access [[Prototype]] in code, it determines how JavaScript resolves property lookups when the property is not found on the object itself.

    prototype

    • The prototype property is associated with constructor functions or classes.
    • When you create a new object using a constructor or class, the prototype of that constructor becomes the [[Prototype]] of the created object.
    • It acts as a blueprint for objects created by the constructor, enabling shared properties and methods.
    function Person() {}
    Person.prototype.greet = function () {
      return "Hello!";
    };
    
    const john = new Person();
    console.log(john.greet()); // Inherits greet from Person.prototype
    

    proto

    • __proto__ is a legacy accessor property that lets you get or set the [[Prototype]] of an object.
    • While widely supported, it is not recommended in modern JavaScript due to potential performance issues.
    • Use Object.getPrototypeOf() and Object.setPrototypeOf() instead.

    Object.getPrototypeOf()

    • This method retrieves the [[Prototype]] of a given object.
    • It is the standard way to access the prototype in modern JavaScript.
    const obj = {};
    console.log(Object.getPrototypeOf(obj)); // Outputs the default object prototype
    

    Object.setPrototypeOf()

    • This method sets or modifies the [[Prototype]] of an object.
    • While useful, it is discouraged in performance-critical applications because it can slow down property access.
    const obj = {};
    const proto = { greet: () => "Hello" };
    
    Object.setPrototypeOf(obj, proto);
    console.log(obj.greet()); // Outputs: "Hello"
    

    Difference Between function and constructor function

    Function

    • A function is a general-purpose block of reusable code that performs a specific task.
    • It is called directly to execute its logic and does not inherently create objects unless explicitly done so.
    • Functions do not have a prototype property designed for object instantiation.
    • Functions are used for logical operations, computations, or tasks, but not typically for object creation in the sense of instantiation.
    • Example:
    function sayHello() {
      console.log("Hello!");
    }
    sayHello(); // Executes the function and logs "Hello!"
    

    Constructor Function

    • A constructor function is a specialized type of function used to create and initialize objects.
    • It is called with the new keyword, which automatically:
      • Creates a new object.
      • Assigns this to the new object.
      • Sets the [[Prototype]] of the new object to the constructor’s prototype.
      • Returns the newly created object (unless another object is explicitly returned).
    • Constructor functions typically have their names capitalized to distinguish them (though this is not mandatory).
    • Example:
    function Person(name) {
      this.name = name;
    }
    const person1 = new Person("John");
    console.log(person1.name); // "John"
    

    What happens behind the scenes when the new keyword is used?

    1. A new object is created: The new keyword initiates the creation of a new JavaScript object.

    2. A prototype is linked: The newly created object is linked to the prototype of the constructor function. This enables the object to inherit properties and methods defined on the constructor’s prototype.

    3. The constructor is called: The constructor function is invoked with the provided arguments, and the this keyword is bound to the newly created object. If the constructor does not explicitly return a value, JavaScript implicitly returns the newly created object.

    4. The new object is returned: If the constructor function does not return a non-primitive value (like an object, array, or function), the newly created object is returned.

    Conclusion

    These are some of the interesting concepts I’ve learned about OOP in JavaScript. If I come across anything new related to this topic, I’ll be sure to update the blog. If I’ve missed any important topics, feel free to mention them!