JavaScript Part 9: Object-oriented Programming In JavaScript (OOP Pillars)

JavaScript Part 9: Object-oriented Programming In JavaScript (OOP Pillars)


Please Subscribe Youtube| Like Facebook | Follow Twitter

Object-Oriented Programming (OOP Pillars) In JavaScript

In this article, we will provide a detailed overview of Object-Oriented Programming In JavaScript (OOP Pillars) and provide examples of how they are used in JavaScript programming.

The Pillars of Object-Oriented Programming

OOP is built upon three key pillars in JavaScript: encapsulation, inheritance, and polymorphism. In this article we explore them as in previous article we explored classes and objects.

Encapsulation

Encapsulation in JavaScript refers to the concept of bundling data and the methods that operate on that data into a single unit, often referred to as a class. It is a way to organize and protect the internal state of an object from being accessed or modified directly from outside the object. Encapsulation helps in creating more modular, maintainable, and secure code by enforcing data hiding and abstraction.

In JavaScript, encapsulation can be achieved through the use of closures and object-oriented programming (OOP) principles. Here’s an example that demonstrates encapsulation in JavaScript:

class Person {
  constructor(name, age) {
    // Private variables
    let _name = name;
    let _age = age;

    // Public methods
    this.getName = function() {
      return _name;
    };

    this.getAge = function() {
      return _age;
    };

    this.setAge = function(newAge) {
      if (newAge >= 0) {
        _age = newAge;
      }
    };
  }
}

// Creating objects
const person1 = new Person('John', 30);
const person2 = new Person('Jane', 25);

// Accessing private variables indirectly
console.log(person1.getName());  // Output: John
console.log(person1.getAge());   // Output: 30

// Modifying private variable indirectly
person1.setAge(35);
console.log(person1.getAge());   // Output: 35

// Direct access to private variables is not possible
console.log(person1._name);      // Output: undefined
console.log(person1._age);       // Output: undefined

Output

John
30
35
undefined
undefined

In this example, the Person class is defined using the class keyword. The private variables _name and _age are declared using the let keyword within the constructor. The public methods getName, getAge, and setAge are defined as instance methods using the this keyword.

By using the class syntax and the concept of lexical scoping in JavaScript, the private variables are encapsulated within each instance of the class. The public methods have access to these private variables, allowing controlled interaction with the encapsulated data.

The example demonstrates how you can indirectly access and modify the private variables using the public methods, while direct access to the private variables _name and _age from outside the class is not possible.

This approach using the class syntax provides a clean and intuitive way to achieve encapsulation in JavaScript.

Encapsulation levels in JavaScript

  1. Private Level: Private encapsulation involves creating private variables and functions within a specific scope using techniques like closures or modules. Private members are inaccessible from outside the scope, allowing you to hide implementation details and protect internal state.
  2. Property Level: Property encapsulation is achieved using getter and setter methods. Getters allow controlled access to retrieve property values, while setters enable custom logic for setting property values. This level of encapsulation helps ensure proper data access and manipulation within objects.
  3. Object Level: Object-level encapsulation involves creating objects with properties and methods that encapsulate related data and behavior. Objects encapsulate their state and provide an interface to interact with that state, ensuring that data and methods are logically grouped together.
function Person(name, age) {
  let _name = name;  // Private variable
  let _age = age;  // Private variable

  // Public methods
  this.getName = function() {
    return _name;
  };

  this.getAge = function() {
    return _age;
  };

  this.setName = function(newName) {
    _name = newName;
  };

  this.setAge = function(newAge) {
    _age = newAge;
  };
}

const person1 = new Person('John', 30);
console.log(person1.getName());  // Output: John
console.log(person1.getAge());   // Output: 30

person1.setName('Kane');
person1.setAge(25);
console.log(person1.getName());  // Output: Kane
console.log(person1.getAge());   // Output: 25

Output

John
30
Kane
25

Private Level:

The variables _name and _age are private variables, accessible only within the Person function scope. They cannot be accessed or modified directly from outside the function.

Property Level:

The public methods getName() and getAge() act as getters, allowing controlled access to retrieve the values of the private variables _name and _age. The methods provide an interface to access the encapsulated data without direct access to the private variables.

Object Level:

Instances of the Person function, such as person1, encapsulate the state (name and age) and behavior within their own context. The public methods setName() and setAge() act as setters, allowing controlled modification of the encapsulated data.

This example demonstrates encapsulation by hiding the private variables and exposing controlled access to them through public methods. It provides a simplified illustration of how encapsulation can be achieved at different levels, enabling data privacy, controlled access, and logical grouping of related data and behavior.

Inheritance

In JavaScript, inheritance is a mechanism that allows objects to inherit properties and methods from a parent object, known as the prototype or superclass. It enables code reuse and promotes a hierarchical structure among objects. In JavaScript, inheritance is primarily achieved through prototype chaining or using the ES6 class syntax. Let’s explore both approaches:

Prototype Chaining

Prototype chaining is a mechanism in which objects inherit properties and methods from their prototype objects. Every JavaScript object has an internal prototype property, denoted as [[Prototype]]. When a property or method is accessed on an object, JavaScript looks for that property or method first in the object itself and then in its prototype chain until it finds the property or reaches the end of the chain.

Here’s an example that demonstrates prototype chaining:

// Parent object constructor
function Animal(name) {
  this.name = name;
}

// Parent object method
Animal.prototype.sayName = function() {
  console.log(`My name is ${this.name}`);
};

// Child object constructor
function Dog(name, breed) {
  Animal.call(this, name);  // Calling the parent constructor
  this.breed = breed;
}

// Inheriting from the parent prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Child object method
Dog.prototype.bark = function() {
  console.log('Woof!');
};

// Creating an instance of the child object
const myDog = new Dog('Buddy', 'Labrador');
myDog.sayName();  // Output: My name is Buddy
myDog.bark();     // Output: Woof!

Output

My name is Buddy
Woof!

In this example, the Animal function serves as the parent object. The Dog function acts as the child object, inheriting from Animal. By using Object.create(), we create a new object with Animal.prototype as its prototype and assign it to Dog.prototype. This establishes the prototype chain, allowing Dog instances to access properties and methods defined in Animal.

ES6 Class Syntax

With the introduction of the ES6 class syntax, JavaScript provides a more familiar and intuitive way to implement inheritance using the class keyword. Under the hood, it still relies on prototype chaining.

Here’s an example that demonstrates inheritance using the ES6 class syntax:

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

  sayName() {
    console.log(`My name is ${this.name}`);
  }
}

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

  bark() {
    console.log('Woof!');
  }
}

// Creating an instance of the child class
const myDog = new Dog('Buddy', 'Labrador');
myDog.sayName();  // Output: My name is Buddy
myDog.bark();     // Output: Woof!

Output

My name is Buddy
Woof!

In this example, the Animal class acts as the parent class, and the Dog class extends it using the extends keyword. The super() method is used to call the parent constructor. The child class inherits the parent class’s properties and methods, and it can also define its own additional properties and methods.

Both the prototype chaining approach and the ES6 class syntax enable inheritance in JavaScript, allowing objects to inherit and reuse properties and methods from a parent object. These mechanisms facilitate code organization, modularity, and code reuse in JavaScript applications.

Super keyword

The super keyword in JavaScript is used within derived classes (child classes) to call and access the parent class’s methods , constructor, and not to directly access properties. It allows derived classes to extend or override the behavior of the parent class while still having access to the parent class’s implementation.

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

  introduce() {
    console.log(`I am ${this.name}.`);
  }
}

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

  displayInfo() {
    super.introduce();  // Calling the parent method
    console.log(`I am a ${this.breed} dog.`);
  }

  fetch() {
    console.log(`${this.name} is fetching.`);  // Accessing the inherited property
  }
}

const myDog = new Dog('Buddy', 'Labrador');
myDog.displayInfo();
// Output:
// I am Buddy.
// I am a Labrador dog.

myDog.fetch();
// Output: Buddy is fetching.

Output

I am Buddy.
I am a Labrador dog.
Buddy is fetching.

The Animal class has a constructor that sets the name property and an introduce() method.
The Dog class extends Animal and includes a constructor that calls the parent constructor using super(name) to initialize the name property. It also has a displayInfo() method that calls the parent introduce() method using super.introduce() and adds additional information about the dog’s breed.
The fetch() method in the Dog class demonstrates accessing the inherited name property directly.
The usage of super allows the Dog class to access and invoke the parent class’s method (introduce()) and access the inherited property (name).

Polymorphism

Polymorphism in JavaScript refers to the ability of objects to exhibit different behaviors based on their specific types while adhering to a common interface. It allows developers to write code that can handle objects of different types in a uniform manner.

In JavaScript, polymorphism can be achieved through method overriding. JavaScript does not support method overloading natively. Method overloading, where multiple methods with the same name but different parameter lists can coexist, is not directly achievable in JavaScript. Instead, you can use conditional statements or optional parameters to simulate different method behaviors based on the provided arguments.

Method Overriding: Method overriding allows a subclass to provide a different implementation of a method that is already defined in its superclass. This enables the subclass to change the behavior of the inherited method.

class Shape {
  constructor() {
    // Common properties and methods for all shapes
  }

  draw() {
    console.log("Drawing a generic shape.");
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  draw() {
    console.log(`Drawing a circle with radius ${this.radius}.`);
  }
}

class Square extends Shape {
  constructor(sideLength) {
    super();
    this.sideLength = sideLength;
  }

  draw() {
    console.log(`Drawing a square with side length ${this.sideLength}.`);
  }
}

// Polymorphic behavior in action
const shapes = [new Circle(5), new Square(10)];

shapes.forEach((shape) => {
  shape.draw(); // The appropriate draw() method is called based on the object's type
});

Output

Drawing a circle with radius 5.
Drawing a square with side length 10.

In this example, we have a base Shape class with a common draw method. The Circle and Square classes inherit from Shape and override the draw method with their specific implementations. When we iterate over an array of shapes and call the draw method, polymorphism allows the correct draw method to be invoked for each object based on its type.

Conclusion

Encapsulation, inheritance, and polymorphism are three fundamental concepts in JavaScript object-oriented programming. Together, they provide a powerful and flexible foundation for creating modular, maintainable, and extensible code.

JavaScript Beginner Tutorial Series

Please Subscribe Youtube| Like Facebook | Follow Twitter


Leave a Reply

Your email address will not be published. Required fields are marked *