Object Oriented Programming - rs-hash/Learning GitHub Wiki

Object Oriented Programming

1. Objects and Classes:

In JavaScript, objects are instances of classes, and classes serve as blueprints for creating objects. Classes can be defined using constructor functions or ES6 classes.

Example: Using ES6 class to define a class and create objects:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
}

const john = new Person("John Doe", 30);
john.sayHello(); // Output: Hello, my name is John Doe and I'm 30 years old.

2. Constructor Functions:

Constructor functions are used to create objects with specific properties and methods. They use the new keyword to instantiate new objects.

Example: Using a constructor function to define a class and create objects:

function Car(make, model) {
  this.make = make;
  this.model = model;
}

Car.prototype.drive = function () {
  console.log(`Driving the ${this.make} ${this.model}.`);
};

const myCar = new Car("Toyota", "Camry");
myCar.drive(); // Output: Driving the Toyota Camry.

3. Properties and Methods:

Properties are attributes that store data, and methods are functions that define an object's behavior.

Example: Properties and methods are demonstrated in the above examples. name, age, make, model are properties, and sayHello() and drive() are methods.

4. Encapsulation:

Encapsulation is the bundling of data and methods within an object to hide the internal implementation details from the outside world.

Example: Using a constructor function to achieve encapsulation:

function Counter() {
  let count = 0;

  this.increment = function () {
    count++;
  };

  this.getCount = function () {
    return count;
  };
}

const counter = new Counter();
counter.increment();
console.log(counter.getCount()); // Output: 1

5. Inheritance:

Inheritance allows objects to inherit properties and methods from other objects, promoting code reuse and hierarchy.

Example: Using prototype inheritance to implement inheritance:

function Animal(name) {
  this.name = name;
}

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

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.sayBreed = function () {
  console.log(`I am a ${this.breed} dog.`);
};

const dog = new Dog("Buddy", "Labrador");
dog.sayName();  // Output: My name is Buddy.
dog.sayBreed(); // Output: I am a Labrador dog.

Class inheritance

In JavaScript, class inheritance allows one class (called the subclass or derived class) to inherit properties and methods from another class (called the superclass or base class). This is achieved using the extends keyword, and the subclass can access the properties and methods of the superclass through its prototype chain.

Let's illustrate class inheritance in JavaScript with an example:

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

  eat() {
    console.log(`${this.name} is eating.`);
  }
}

// Subclass (Derived class) inheriting from Animal
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call the constructor of the superclass
    this.breed = breed;
  }

  bark() {
    console.log(`Woof! I am a ${this.breed} dog.`);
  }
}

// Creating an instance of the Dog class
const dog = new Dog("Buddy", "Labrador");

// Calling methods of both the subclass and superclass
dog.eat(); // Output: Buddy is eating.
dog.bark(); // Output: Woof! I am a Labrador dog.

console.log(dog.name); // Output: Buddy (inherited from Animal superclass)
console.log(dog.breed); // Output: Labrador

In the above example, we have a superclass Animal that has a constructor and a method eat(). The subclass Dog is created using the extends keyword, inheriting the properties and methods of the Animal class. The super() method is used inside the constructor of the subclass to call the constructor of the superclass and initialize the shared properties.

The Dog subclass also has its own method bark(), which is specific to dogs and not present in the Animal class.

When we create an instance of the Dog class (e.g., const dog = new Dog("Buddy", "Labrador");), we can call both the methods of the subclass (eat() and bark()) as well as access the properties (name inherited from Animal and breed specific to Dog).

Class inheritance in JavaScript allows us to build hierarchical relationships between classes, promoting code reuse and providing an efficient way to model real-world entities and their behaviors.

6. Prototype Chain:

The prototype chain allows objects to inherit properties and methods from their prototype objects.

Example: The prototype chain is demonstrated in the inheritance example above, where Dog inherits from Animal.

7. Polymorphism:

Polymorphism allows objects of different classes to be treated as instances of a common superclass.

Example: Using polymorphism with interfaces (emulated in JavaScript):

class Shape {
  area() {
    return 0;
  }
}

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

  area() {
    return Math.PI * this.radius * this.radius;
  }
}

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

  area() {
    return this.side * this.side;
  }
}

function printArea(shape) {
  console.log(`Area: ${shape.area()}`);
}

const circle = new Circle(5);
const square = new Square(4);

printArea(circle); // Output: Area: 78.53981633974483
printArea(square); // Output: Area: 16

8. Abstraction:

Abstraction involves simplifying complex behavior, exposing only relevant details of an object while hiding unnecessary implementation details.

Example: Using abstraction to create a simple car class:

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

  drive() {
    console.log(`Driving the ${this.make} ${this.model}.`);
  }
}

const myCar = new Car("Toyota", "Camry");
myCar.drive(); // Output: Driving the Toyota Camry.

9. Composition:

Composition is a design principle where an object contains other objects as its properties. It allows creating complex objects by combining simpler ones.

Example:

function Engine() {
  this.start = function () {
    console.log("Engine started.");
  };
}

function Car(make, model) {
  this.make = make;
  this.model = model;
  this.engine = new Engine();
}

const myCar = new Car("Toyota", "Camry");
myCar.engine.start(); // Output: Engine started.

10. Constructor Chaining:

Constructor chaining is the process of calling a constructor function of a parent class from the constructor of a child class, ensuring that both parent and child class setup logic is executed.

Example: Achieving constructor chaining in JavaScript using super():

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

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

const dog = new Dog("Buddy", "Labrador");
console.log(dog.name); // Output: Buddy
console.log(dog.breed); // Output: Labrador

11. Static Members:

Static members (properties and methods) belong to the class itself rather than instances of the class. They can be accessed using the class name and are not specific to any particular instance.

Example: Using static method in a class:

class MathUtil {
  static square(x) {
    return x * x;
  }
}

console.log(MathUtil.square(5)); // Output: 25

12. Factory Pattern:

The factory pattern is a creational pattern that provides an interface for creating objects but allows subclasses to alter the type of objects that will be created.

Example: Using a factory pattern to create different types of shapes:

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

class Circle extends Shape {
  draw() {
    console.log("Drawing a circle.");
  }
}

class Square extends Shape {
  draw() {
    console.log("Drawing a square.");
  }
}

function createShape(type) {
  switch (type) {
    case "circle":
      return new Circle();
    case "square":
      return new Square();
    default:
      return new Shape();
  }
}

const circle = createShape("circle");
const square = createShape("square");

circle.draw(); // Output: Drawing a circle.
square.draw(); // Output: Drawing a square.

13. Singleton Pattern:

The singleton pattern ensures that a class has only one instance and provides a global point of access to that instance.

Example: Creating a singleton Logger class:

class Logger {
  constructor() {
    if (!Logger.instance) {
      Logger.instance = this;
    }

    return Logger.instance;
  }

  log(message) {
    console.log(message);
  }
}

const logger1 = new Logger();
const logger2 = new Logger();

console.log(logger1 === logger2); // Output: true (same instance)

14. Mixin:

A mixin is a way to combine multiple objects and functionalities into a single object, allowing for code reuse and composition.

Example: Creating a mixin to add logging capabilities to objects:

const logMixin = {
  log(message) {
    console.log(message);
  }
};

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

// Mixing the logMixin into the Car class
Object.assign(Car.prototype, logMixin);

const myCar = new Car("Toyota", "Camry");
myCar.log("Car started."); // Output: Car started.

These examples illustrate various object-oriented programming concepts in JavaScript. Understanding and applying these concepts will enable you to write more organized, scalable, and maintainable code using object-oriented principles in your JavaScript applications.

Tricky questions

Sure! Here are tricky object-oriented interview questions in JavaScript along with their answers:

1. Prototypal Inheritance vs. Classical Inheritance: Question: Explain the difference between prototypal inheritance and classical inheritance in JavaScript. Provide examples to illustrate each.

Answer: Prototypal inheritance is based on prototype chains, where objects inherit properties and methods directly from other objects. Classical inheritance, on the other hand, is based on classes and uses a hierarchical class-based structure. In JavaScript, prototypal inheritance is used, and classes are introduced as syntactic sugar over prototype chains.

Prototypal Inheritance Example:

function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function () {
  console.log(`${this.name} is eating.`);
};

function Dog(name, breed) {
  this.name = name;
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

const dog = new Dog("Buddy", "Labrador");
dog.eat(); // Output: Buddy is eating.

Classical Inheritance Example (ES6 Class Syntax):

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

  eat() {
    console.log(`${this.name} is eating.`);
  }
}

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

const dog = new Dog("Buddy", "Labrador");
dog.eat(); // Output: Buddy is eating.

2. Private Members in JavaScript Classes: Question: How do you create private members (variables and methods) in JavaScript classes? Provide an example demonstrating encapsulation.

Answer: JavaScript does not have built-in support for private members, but you can achieve encapsulation and create private variables and methods using closures.

Example:

class Counter {
  constructor() {
    let count = 0; // Private variable

    // Private method
    function increment() {
      count++;
    }

    // Public method to access and modify private data
    this.getCount = function () {
      return count;
    };

    this.incrementCount = function () {
      increment();
    };
  }
}

const counter = new Counter();
counter.incrementCount();
console.log(counter.getCount()); // Output: 1

3. Multiple Inheritance in JavaScript: Question: JavaScript supports single inheritance through prototype chains. How can you achieve multiple inheritance-like behavior in JavaScript? Provide an example using mixins or other techniques.

Answer: Multiple inheritance can be emulated in JavaScript using mixins, where objects can be combined to share properties and methods from multiple sources.

Example using mixins:

const canSwim = {
  swim() {
    console.log("Swimming...");
  }
};

const canFly = {
  fly() {
    console.log("Flying...");
  }
};

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

// Mixing the behaviors of canSwim and canFly into Duck class
Object.assign(Duck.prototype, canSwim, canFly);

const donaldDuck = new Duck("Donald");
donaldDuck.swim(); // Output: Swimming...
donaldDuck.fly();  // Output: Flying...

4. ES6 Class Extends vs. Object.assign: Question: What is the difference between using extends to create class inheritance and using Object.assign to achieve composition in JavaScript? Discuss the advantages and disadvantages of each approach.

Answer: extends is used for class inheritance, where a subclass extends a superclass. Object.assign is used for object composition, where properties and methods of one or more source objects are copied into a target object.

extends Class Inheritance Example:

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

  eat() {
    console.log(`${this.name} is eating.`);
  }
}

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

const dog = new Dog("Buddy", "Labrador");
dog.eat(); // Output: Buddy is eating.

Object.assign Composition Example:

const canSwim = {
  swim() {
    console.log("Swimming...");
  }
};

const canFly = {
  fly() {
    console.log("Flying...");
  }
};

const duck = {};
Object.assign(duck, canSwim, canFly);

duck.swim(); // Output: Swimming...
duck.fly();  // Output: Flying...

5. Class Constructor Invocation: Question: Explain what happens when a class is instantiated without a constructor defined. How does JavaScript handle this situation, and what is the default behavior?

Answer: When a class is instantiated without a constructor defined, JavaScript provides a default constructor implicitly. This default constructor does not initialize any properties, and the class instance will have an empty object as its state.

Example:

class MyClass {}

const myObj = new MyClass();
console.log(myObj); // Output: MyClass {}

6. Prototype Pollution: Question: What is prototype pollution in JavaScript, and how can it lead to security vulnerabilities? Provide an example of how prototype pollution can be exploited.

Answer: Prototype pollution is a vulnerability that arises when attackers can modify the prototype of a target object, causing unintended modifications to the object's properties or behavior.

Example of Prototype Pollution:

const user = {};

// Malicious data from an untrusted source
const maliciousData = '{"__proto__": {"isAdmin": true}}';
const parsedData = JSON.parse(maliciousData);

// Prototype of user object is polluted
Object.assign(user, parsedData);

console.log(user.isAdmin); // Output: true

**7. Polymorphism in JavaScript:**
Polymorphism is a core concept in Object-Oriented Programming (OOP) that allows objects of different classes to be treated as instances of a common superclass. In JavaScript, polymorphism can be achieved through interfaces (emulated using shared methods) or duck typing, which focuses on the behavior of objects rather than their class hierarchy.

**Example:**
Using interfaces to achieve polymorphism:
```javascript
// Interface (emulated using shared methods)
const ShapeInterface = {
calculateArea() {
 throw new Error("calculateArea() must be implemented.");
},
};

// Classes implementing the interface
class Circle {
constructor(radius) {
 this.radius = radius;
}

calculateArea() {
 return Math.PI * this.radius * this.radius;
}
}

class Square {
constructor(side) {
 this.side = side;
}

calculateArea() {
 return this.side * this.side;
}
}

function printArea(shape) {
console.log(`Area: ${shape.calculateArea()}`);
}

const circle = new Circle(5);
const square = new Square(4);

printArea(circle); // Output: Area: 78.53981633974483
printArea(square); // Output: Area: 16

8. Understanding super() in Classes: The super() method in a subclass constructor is used to call the constructor of the superclass. It ensures that the setup logic of both the subclass and the superclass is executed.

Example:

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 the constructor of the superclass (Animal)
    this.breed = breed;
  }

  speak() {
    super.speak(); // Call the speak() method of the superclass (Animal)
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog("Buddy", "Labrador");
dog.speak();
// Output:
// Buddy makes a sound.
// Buddy barks.

9. Object Creation Patterns:

  • Factory Pattern: A factory function returns objects without using classes. It encapsulates object creation logic and provides a way to create instances with different configurations.

  • Constructor Pattern: Uses constructor functions to create objects. The new keyword is used to create instances, and this refers to the newly created object.

  • Module Pattern: Encapsulates private data and methods using closures and returns an object with public methods, providing a way to create private members in JavaScript.

  • Prototype Pattern: Uses prototypes to share methods and properties among objects, promoting memory efficiency as shared methods are not duplicated for each instance.

10. Closures and Memory Management: Closures can lead to memory leaks if not managed properly. When a function inside a closure refers to variables outside its scope, those variables are not garbage collected as long as the closure exists.

Scenario: Event listeners are a common cause of memory leaks. If an event listener is not removed when it is no longer needed, it keeps a reference to the closure, preventing its variables from being garbage collected.

11. Extending Built-in Objects: Extending built-in objects can be risky as it may lead to conflicts with future JavaScript updates or third-party libraries. It's generally considered a best practice to avoid extending native prototypes.

To avoid name collisions, use a unique prefix for the methods or properties added to built-in objects, or use a utility library that provides similar functionality without modifying the prototypes.

12. Mixins and Composition: Mixins are a way to combine multiple objects and functionalities into a single object, allowing for code reuse and composition. Mixins are used to create flexible and reusable object structures.

Example:

// Mixin for logging functionality
const logMixin = {
  log(message) {
    console.log(message);
  }
};

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

// Mixing logMixin into Dog class
Object.assign(Dog.prototype, logMixin);

const dog = new Dog("Buddy");
dog.log("Hello!"); // Output: Hello!

13. Changing the Prototype of an Object: It is generally not safe to directly change the prototype of an existing object in JavaScript, as it may lead to unexpected behavior and can break the prototype chain.

If you need to add new methods or properties to an object, it's better to use Object.assign() to extend the object or create a new object with the desired prototype.

14. Class Inheritance vs. Object Composition: Class inheritance creates a hierarchical relationship between classes, where subclasses extend the functionality of the superclass. Object composition creates more flexible structures by combining multiple objects to form a new object.

Class inheritance is suitable for modeling hierarchical relationships and sharing common behavior, while object composition is useful for creating complex objects with different functionalities.

15. Difference between class extends and Object.assign: class extends is used for class inheritance, where one class (subclass) extends another class (superclass). The subclass inherits properties and methods from the superclass using prototype chains.

Object.assign() is used for object composition, where properties and methods of multiple source objects are copied into a target object.

The key difference is that class extends creates a hierarchical relationship between classes, while Object.assign() combines objects to form a new object without any hierarchical structure.