7. Arrow Functions and Lexical this

In arrow functions, this is lexically scoped, meaning it inherits this from the surrounding context.

const person = {  name: \\\"Bob\\\",  greet: function () {    const arrowFunc = () => {      console.log(this.name); // \\'this\\' refers to the \\'person\\' object    };    arrowFunc();  },};person.greet(); // Output: Bob
  1. call, apply, and bind MethodsYou can explicitly set the value of this using call, apply, or bind.
// callfunction greet() {  console.log(`Hello, ${this.name}`);}const user = { name: \\\"John\\\" };greet.call(user); // Output: Hello, John// apply (similar to call but with arguments as an array):function introduce(greeting, age) {  console.log(`${greeting}, I\\'m ${this.name} and I\\'m ${age} years old.`);}const user = { name: \\\"Emily\\\" };introduce.apply(user, [\\\"Hi\\\", 25]); // Output: Hi, I\\'m Emily and I\\'m 25 years old.// bind (returns a new function with this bound):function sayName() {  console.log(this.name);}const user = { name: \\\"Lucy\\\" };const boundFunc = sayName.bind(user);boundFunc(); // Output: Lucy

Static Methods and Properties

Static methods and properties belong to the class itself rather than to instances of the class. Remember, we said earlier that whenever you instantiate a class, it is an instance of the class that is created, not the class itself. Based on this, we can say that static methods and properties are used to create methods and properties that are related to the class but not to any particular instance of the class.

class MathOperations {  static PI = 3.14159;  static square(x) {    return x * x;  }  static cube(x) {    return x * x * x;  }}// Accessing static properties and methods directly from the classconsole.log(MathOperations.PI); // 3.14159console.log(MathOperations.square(4)); // 16console.log(MathOperations.cube(3)); // 27// Accessing static properties and methods through an instance (This will not work)const mathOperations = new MathOperations();console.log(mathOperations.PI); // undefinedconsole.log(mathOperations.square(4)); // TypeError: mathOperations.square is not a functionconsole.log(mathOperations.cube(3)); // TypeError: mathOperations.cube is not a function

Private and Public Properties/Methods

JavaScript has several ways to implement private properties and methods:

Using Symbols

const _radius = Symbol(\\\"radius\\\");class Circle {  constructor(radius) {    this[_radius] = radius;  }  get area() {    return Math.PI * this[_radius] ** 2;  }}const circle = new Circle(5);console.log(circle.area); // 78.53981633974483console.log(circle[_radius]); // undefined (the property is private - can not be accessed outside the class)

Using WeakMaps

const _radius = new WeakMap();class Circle {  constructor(radius) {    _radius.set(this, radius);  }  get area() {    return Math.PI * _radius.get(this) ** 2;  }}const circle = new Circle(5);console.log(circle.area); // 78.53981633974483console.log(_radius.get(circle)); // 5

Using Private Fields (ES2022)

class Circle {  #radius;  constructor(radius) {    this.#radius = radius;  }  get area() {    return Math.PI * this.#radius ** 2;  }}const circle = new Circle(5);console.log(circle.area); // 78.53981633974483// console.log(circle.#radius); // SyntaxError

Getters and Setters

Getters and setters allow you to define object accessors (computed properties):

class Temperature {  constructor(celsius) {    this._celsius = celsius;  }  get fahrenheit() {    return (this._celsius * 9) / 5   32;  }  set fahrenheit(value) {    this._celsius = ((value - 32) * 5) / 9;  }  get celsius() {    return this._celsius;  }  set celsius(value) {    if (value < -273.15) {      throw new Error(\\\"Temperature below absolute zero is not possible\\\");    }    this._celsius = value;  }}const temp = new Temperature(25);console.log(temp.fahrenheit); // 77temp.fahrenheit = 86;console.log(temp.celsius); // 30

Polymorphism and Method Overriding

Polymorphism allows objects of different types to be treated as objects of a common parent class. Method overriding is a form of polymorphism where a subclass provides a specific implementation of a method that is already defined in its parent class.

class Shape {  area() {    return 0;  }  toString() {    return `Area: ${this.area()}`;  }}class Circle extends Shape {  constructor(radius) {    super();    this.radius = radius;  }  area() {    return Math.PI * this.radius ** 2;  }}class Rectangle extends Shape {  constructor(width, height) {    super();    this.width = width;    this.height = height;  }  area() {    return this.width * this.height;  }}const shapes = [new Circle(5), new Rectangle(4, 5)];shapes.forEach((shape) => {  console.log(shape.toString());});// Output:// Area: 78.53981633974483// Area: 20

Notice how both the Circle and Rectangle classes have a toString method (which we inherited from the Shape class - the parent class). However, the toString method in the Circle class overrides the toString method in the Shape class. This is an example of polymorphism and method overriding.

Object Freezing, Sealing, and Preventing Extensions

// Object.freeze() - Prevents adding, removing, or modifying propertiesconst frozenObj = Object.freeze({  prop: 42,});frozenObj.prop = 33; // Fails silently in non-strict modeconsole.log(frozenObj.prop); // 42// Object.seal() - Prevents adding new properties and marking existing properties as non-configurableconst sealedObj = Object.seal({  prop: 42,});sealedObj.prop = 33; // This workssealedObj.newProp = \\\"new\\\"; // This fails silently in non-strict modeconsole.log(sealedObj.prop); // 33console.log(sealedObj.newProp); // undefined// Object.preventExtensions() - Prevents adding new propertiesconst nonExtensibleObj = Object.preventExtensions({  prop: 42,});nonExtensibleObj.prop = 33; // This worksnonExtensibleObj.newProp = \\\"new\\\"; // This fails silently in non-strict modeconsole.log(nonExtensibleObj.prop); // 33console.log(nonExtensibleObj.newProp); // undefined

These methods are useful for creating immutable objects or preventing accidental modifications to objects.

Best Practices for Writing Clean OOP Code in JavaScript

  1. Use ES6 Classes: They provide a cleaner, more intuitive syntax for creating objects and implementing inheritance.

  2. Follow the Single Responsibility Principle: Each class should have a single, well-defined purpose.

// Good ✅class User {  constructor(name, email) {    this.name = name;    this.email = email;  }}class UserValidator {  static validateEmail(email) {    // Email validation logic  }}// Not so good ❌class User {  constructor(name, email) {    this.name = name;    this.email = email;  }  validateEmail() {    // Email validation logic  }}
  1. Use Composition Over Inheritance: Favor object composition over class inheritance when designing larger systems.
// Compositionclass Engine {  start() {    /* ... */  }}class Car {  constructor() {    this.engine = new Engine();  }  start() {    this.engine.start();  }}// Inheritanceclass Vehicle {  start() {    /* ... */  }}class Car extends Vehicle {  // ...}
  1. Implement Private Fields: Use the latest JavaScript features or closures to create truly private fields.

  2. Use Getters and Setters: They provide more control over how properties are accessed and modified.

  3. Avoid Overusing this: Use object destructuring in methods to make the code cleaner and less prone to errors.

class Rectangle {  constructor(width, height) {    this.width = width;    this.height = height;  }  area() {    const { width, height } = this;    return width * height;  }}
  1. Use Method Chaining: It can make your code more readable and concise.
class Calculator {  constructor() {    this.value = 0;  }  add(n) {    this.value  = n;    return this;  }  subtract(n) {    this.value -= n;    return this;  }  result() {    return this.value;  }}const calc = new Calculator();console.log(calc.add(5).subtract(2).result()); // 3
  1. Favor Declarative Over Imperative Programming: Use higher-order functions like map, filter, and reduce when working with collections.

  2. Use Static Methods Appropriately: Use static methods for utility functions that don\\'t require access to instance-specific data.

  3. Write Self-Documenting Code: Use clear, descriptive names for classes, methods, and properties. Add comments only when necessary to explain complex logic.

Small Project: Building a Library Management System

Let\\'s put our OOP knowledge into practice by building a simple Library Management System.

class Book {  constructor(title, author, isbn) {    this.title = title;    this.author = author;    this.isbn = isbn;    this.isAvailable = true;  }  checkout() {    if (this.isAvailable) {      this.isAvailable = false;      return true;    }    return false;  }  return() {    this.isAvailable = true;  }}class Library {  constructor() {    this.books = [];  }  addBook(book) {    this.books.push(book);  }  findBookByISBN(isbn) {    return this.books.find((book) => book.isbn === isbn);  }  checkoutBook(isbn) {    const book = this.findBookByISBN(isbn);    if (book) {      return book.checkout();    }    return false;  }  returnBook(isbn) {    const book = this.findBookByISBN(isbn);    if (book) {      book.return();      return true;    }    return false;  }  get availableBooks() {    return this.books.filter((book) => book.isAvailable);  }}// Usageconst library = new Library();library.addBook(  new Book(\\\"The Great Gatsby\\\", \\\"F. Scott Fitzgerald\\\", \\\"9780743273565\\\"));library.addBook(  new Book(\\\"To Kill a Mockingbird\\\", \\\"Harper Lee\\\", \\\"9780446310789\\\"));console.log(library.availableBooks.length); // 2library.checkoutBook(\\\"9780743273565\\\");console.log(library.availableBooks.length); // 1library.returnBook(\\\"9780743273565\\\");console.log(library.availableBooks.length); // 2

This project demonstrates the use of classes, encapsulation, methods, and properties in a real-world scenario.

Some Leetcode Problems on OOP

To further practice your OOP skills in JavaScript, try solving these problems:

  1. LeetCode: Design Parking System
  2. LeetCode: Design HashMap
  3. Codewars: Object Oriented Piracy

Conclusion

Object-Oriented Programming is a powerful paradigm that helps organize and structure code in a way that mirrors real-world objects and relationships. In this article, we\\'ve covered the fundamental concepts of OOP in JavaScript, from basic object creation to advanced topics like polymorphism and best practices.

Key takeaways:

As you continue your journey with OOP in JavaScript, remember that practice is key. Try to apply these concepts in your projects, refactor existing code to follow OOP principles, and don\\'t be afraid to explore advanced patterns and techniques.

References

For further reading and practice, check out these resources:

  1. MDN Web Docs: Object-oriented JavaScript
  2. JavaScript.info: Classes
  3. You Don\\'t Know JS: this & Object Prototypes
  4. Eloquent JavaScript: Chapter 6: The Secret Life of Objects

Remember, mastering OOP is a journey. Keep coding, keep learning, and most importantly, enjoy the process of creating robust and elegant object-oriented JavaScript applications!



Stay Updated and Connected

To ensure you don\\'t miss any part of this series and to connect with me for more in-depth discussions on Software Development (Web, Server, Mobile or Scraping / Automation), OOP, data structures and algorithms, and other exciting tech topics, follow me on:

Stay tuned and happy coding ?‍??

","image":"http://www.luping.net/uploads/20241017/1729167847671101e7b9d8e.jpg","datePublished":"2024-11-08T16:37:10+08:00","dateModified":"2024-11-08T16:37:10+08:00","author":{"@type":"Person","name":"luping.net","url":"https://www.luping.net/articlelist/0_1.html"}}
「労働者が自分の仕事をうまくやりたいなら、まず自分の道具を研ぎ澄まさなければなりません。」 - 孔子、「論語。陸霊公」
表紙 > プログラミング > オブジェクト指向プログラミング: DSA をマスターするための第一歩

オブジェクト指向プログラミング: DSA をマスターするための第一歩

2024 年 11 月 8 日に公開
ブラウズ:242

Imagine you're walking through a bustling factory. You see different machines, each designed for a specific purpose, working together to create a final product. Some machines are similar but with slight modifications to perform specialized tasks. There's a clear organization: each machine encapsulates its own functionality, yet they inherit common traits from their predecessors, and they can be easily replaced or upgraded without disrupting the entire production line.

This factory is a perfect analogy for Object-Oriented Programming (OOP). In the world of code, our objects are like these machines – self-contained units with specific purposes, inheriting traits, and working together to build complex applications. Just as a factory manager organizes machines for efficient production, OOP helps developers organize code for efficient, maintainable, and scalable software development.

Course Outline

In this article, we'll explore the intricate world of OOP in JavaScript in our pursuit of mastering data structures and algorithms, covering:

  1. What is OOP and why it matters
  2. Key concepts of OOP
    1. Encapsulation
    2. Inheritance
    3. Polymorphism
    4. Abstraction
  3. Objects and Classes in JavaScript
  4. Methods and Properties
  5. Constructor Functions and the new keyword
  6. this keyword and context in OOP
  7. Static methods and properties
  8. Private and public properties/methods (including symbols and weak maps)
  9. Getters and Setters
  10. Polymorphism and method overriding
  11. Object freezing, sealing, and preventing extensions
  12. Best practices for writing clean OOP code in JavaScript
  13. Small Project: Building a Library Management System
  14. Some Leetcode Problems on OOP
  15. Conclusion
  16. References


Let's dive in and start building our own code factory!

Object-Oriented Programming: Your First Step Toward Mastering DSA

What is OOP and Why It Matters

Object-Oriented Programming is a programming paradigm that organizes code into objects, which are instances of classes. These objects contain data in the form of properties and code in the form of methods. OOP provides a structure for programs, making them more organized, flexible, and easier to maintain.

To illustrate OOP, let's consider a real-world example: A Car. In OOP terms, we can think of a car as an object with properties (like color, model, year) and methods (like start, accelerate, brake). Here's how we might represent this in JavaScript:

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

  start() {
    console.log(`The ${this.color} ${this.model} is starting.`);
  }

  accelerate() {
    console.log(`The ${this.color} ${this.model} is accelerating.`);
  }

  brake() {
    console.log(`The ${this.color} ${this.model} is braking.`);
  }
}

const myCar = new Car("red", "Toyota", 2020);
myCar.start(); // The red Toyota is starting.
myCar.accelerate(); // The red Toyota is accelerating.
myCar.brake(); // The red Toyota is braking.

Why does OOP matter?

  1. Organization: OOP helps in organizing complex code into manageable, reusable structures.
  2. Modularity: Objects can be separated and maintained independently, making debugging and updating easier.
  3. Reusability: Once an object is created, it can be reused in different parts of the program or even in different programs.
  4. Scalability: OOP makes it easier to build and maintain larger applications.
  5. Real-world modeling: OOP concepts often align well with real-world objects and scenarios (just like our car example), making it intuitive to model complex systems.

Key Concepts of OOP

In OOP, there are four key concepts that we cannot ignore, they are:

1. Encapsulation

Encapsulation is the bundling of data and the methods that operate on that data within a single unit (object). It restricts direct access to some of an object's components, which is a means of preventing accidental interference and misuse of the methods and data.

Object-Oriented Programming: Your First Step Toward Mastering DSA

class BankAccount {
  #balance = 0; // Private field; it can only be accessed within the class
  // private balance = 0; // this is the same as #balance

  deposit(amount) {
    if (amount > 0) {
      this.#balance  = amount;
      console.log(`Deposited ${amount}. New balance: ${this.#balance}`);
    }
  }

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

const account = new BankAccount();
account.deposit(100);
console.log(account.getBalance()); // 100
// console.log(account.#balance); // This would throw an error

In this example, #balance is a private field, encapsulated within the BankAccount class. It can only be accessed and modified through the class methods, ensuring data integrity.

2. Inheritance

Inheritance allows a class to inherit properties and methods from another class. This promotes code reuse and establishes a relationship between parent and child classes.

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

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

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog("Buddy");
dog.speak(); // Outputs: Buddy barks.

Here, Dog inherits from Animal (since Animal is the parent class, meaning all Dog objects are also Animal objects with their own name property), reusing the name property and overriding the speak method.

3. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common parent class. It enables the same interface to be used for different underlying forms (data types).

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

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

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

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }
}

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

const circle = new Circle(5);
const rectangle = new Rectangle(4, 5);

printArea(circle); // Area: 78.53981633974483
printArea(rectangle); // Area: 20

In this example, printArea function can work with any shape that has an area method, demonstrating polymorphism. Since Circle and Rectangle are both shapes, they are expected to have an area method, though they may have different implementations.

4. Abstraction

Abstraction is the process of hiding complex implementation details and showing only the necessary features of an object. It helps in reducing programming complexity and effort.

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

  start() {
    return "Vehicle started";
  }

  stop() {
    return "Vehicle stopped";
  }
}

class Car extends Vehicle {
  start() {
    return `${this.make} ${this.model} engine started`;
  }
}

const myCar = new Car("Toyota", "Corolla");
console.log(myCar.start()); // Toyota Corolla engine started
console.log(myCar.stop()); // Vehicle stopped

Here, Vehicle provides an abstraction for different types of vehicles. The Car class uses this abstraction and provides its own implementation where needed.

Objects and Classes in JavaScript

In JavaScript, objects are standalone entities with properties and methods. Classes, introduced in ES6, provide a cleaner, more compact alternative to constructor functions and prototypes. Let's explore both approaches:

Objects

Objects can be created using object literals:

const person = {
  name: "John",
  age: 30,
  greet() {
    console.log(`Hello, my name is ${this.name}`);
  },
};

console.log(person.name); // John
person.greet(); // Hello, my name is John

Classes

Classes are templates for creating objects. This means that they define the structure and behavior that all instances of the class will have. In other words, classes serve as blueprints for creating multiple objects with similar properties and methods. When you create an object from a class (using the new keyword), you're creating an instance of that class, which inherits all the properties and methods defined in the class.

Note: It is important to note that when you instantiate a class, the constructor method is called automatically. This method is used to initialize the object's properties. Also, it is just an instance that is been created when you use the new keyword.

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

  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

const john = new Person("John", 30);
john.greet(); // Hello, my name is John

Methods and Properties

Methods are functions that belong to an object, while properties are the object's data.

class Car {
  constructor(make, model) {
    this.make = make; // Property
    this.model = model; // Property
    this.speed = 0; // Property
  }

  // Method
  accelerate(amount) {
    this.speed  = amount;
    console.log(`${this.make} ${this.model} is now going ${this.speed} mph`);
  }

  // Method
  brake(amount) {
    this.speed = Math.max(0, this.speed - amount);
    console.log(`${this.make} ${this.model} slowed down to ${this.speed} mph`);
  }
}

const myCar = new Car("Tesla", "Model 3");
myCar.accelerate(50); // Tesla Model 3 is now going 50 mph
myCar.brake(20); // Tesla Model 3 slowed down to 30 mph

Constructor Functions and the new Keyword

Before ES6 classes, constructor functions were used to create objects:

function Person(name, age) {
  this.name = name;
  this.age = age;

  this.greet = function () {
    console.log(`Hello, my name is ${this.name}`);
  };
}

const john = new Person("John", 30);
john.greet(); // Hello, my name is John

The new keyword:

  1. Creates a new empty object
  2. Sets this to point to that object (the newly created object can now be accessed using the this keyword)
  3. Calls the constructor function to initialize the object
  4. Returns the object (implicitly)

this Keyword and Context in OOP

In JavaScript, this refers to the object that is executing the current function. Its value can change depending on how a function is called. Let's take a look at some examples:

1. Global Context

When used in the global context (outside any function or object), this refers to the global object (window in browsers or global in Node.js).

console.log(this); // In browsers, this will log the 'window' object

2. Inside an Object Method

When this is used inside a method of an object, it refers to the object that owns the method.

const person = {
  name: "Alice",
  sayHello: function () {
    console.log(this.name); // 'this' refers to the 'person' object
  },
};

person.sayHello(); // Output: Alice

3. Inside a Regular Function

In a regular function, this refers to the global object (window in browsers or global in Node.js), unless in strict mode (use strict), where this is undefined.

function showThis() {
  console.log(this); // 'this' refers to the global object in non-strict mode
}

showThis(); // In browsers, it logs the 'window' object

4. Inside a Constructor Function

When using a constructor function, this refers to the newly created object.

function Car(brand) {
  this.brand = brand;
}

const myCar = new Car("Toyota");
console.log(myCar.brand); // Output: Toyota

5. Inside a Class

When used in a class method, this refers to the instance of the class.

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

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

const dog = new Animal("Dog");
dog.speak(); // Output: Dog makes a sound.

6. Using this in an Event Handler

In event handlers, this refers to the HTML element that received the event




7. Arrow Functions and Lexical this

In arrow functions, this is lexically scoped, meaning it inherits this from the surrounding context.

const person = {
  name: "Bob",
  greet: function () {
    const arrowFunc = () => {
      console.log(this.name); // 'this' refers to the 'person' object
    };
    arrowFunc();
  },
};

person.greet(); // Output: Bob
  1. call, apply, and bind Methods You can explicitly set the value of this using call, apply, or bind.
// call
function greet() {
  console.log(`Hello, ${this.name}`);
}

const user = { name: "John" };

greet.call(user); // Output: Hello, John

// apply (similar to call but with arguments as an array):
function introduce(greeting, age) {
  console.log(`${greeting}, I'm ${this.name} and I'm ${age} years old.`);
}

const user = { name: "Emily" };

introduce.apply(user, ["Hi", 25]); // Output: Hi, I'm Emily and I'm 25 years old.

// bind (returns a new function with this bound):
function sayName() {
  console.log(this.name);
}

const user = { name: "Lucy" };
const boundFunc = sayName.bind(user);

boundFunc(); // Output: Lucy

Static Methods and Properties

Static methods and properties belong to the class itself rather than to instances of the class. Remember, we said earlier that whenever you instantiate a class, it is an instance of the class that is created, not the class itself. Based on this, we can say that static methods and properties are used to create methods and properties that are related to the class but not to any particular instance of the class.

class MathOperations {
  static PI = 3.14159;

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

  static cube(x) {
    return x * x * x;
  }
}

// Accessing static properties and methods directly from the class
console.log(MathOperations.PI); // 3.14159
console.log(MathOperations.square(4)); // 16
console.log(MathOperations.cube(3)); // 27

// Accessing static properties and methods through an instance (This will not work)
const mathOperations = new MathOperations();
console.log(mathOperations.PI); // undefined
console.log(mathOperations.square(4)); // TypeError: mathOperations.square is not a function
console.log(mathOperations.cube(3)); // TypeError: mathOperations.cube is not a function

Private and Public Properties/Methods

JavaScript has several ways to implement private properties and methods:

Using Symbols

const _radius = Symbol("radius");

class Circle {
  constructor(radius) {
    this[_radius] = radius;
  }

  get area() {
    return Math.PI * this[_radius] ** 2;
  }
}

const circle = new Circle(5);
console.log(circle.area); // 78.53981633974483
console.log(circle[_radius]); // undefined (the property is private - can not be accessed outside the class)

Using WeakMaps

const _radius = new WeakMap();

class Circle {
  constructor(radius) {
    _radius.set(this, radius);
  }

  get area() {
    return Math.PI * _radius.get(this) ** 2;
  }
}

const circle = new Circle(5);
console.log(circle.area); // 78.53981633974483
console.log(_radius.get(circle)); // 5

Using Private Fields (ES2022)

class Circle {
  #radius;

  constructor(radius) {
    this.#radius = radius;
  }

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

const circle = new Circle(5);
console.log(circle.area); // 78.53981633974483
// console.log(circle.#radius); // SyntaxError

Getters and Setters

Getters and setters allow you to define object accessors (computed properties):

class Temperature {
  constructor(celsius) {
    this._celsius = celsius;
  }

  get fahrenheit() {
    return (this._celsius * 9) / 5   32;
  }

  set fahrenheit(value) {
    this._celsius = ((value - 32) * 5) / 9;
  }

  get celsius() {
    return this._celsius;
  }

  set celsius(value) {
    if (value 



Polymorphism and Method Overriding

Polymorphism allows objects of different types to be treated as objects of a common parent class. Method overriding is a form of polymorphism where a subclass provides a specific implementation of a method that is already defined in its parent class.

class Shape {
  area() {
    return 0;
  }

  toString() {
    return `Area: ${this.area()}`;
  }
}

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

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

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }
}

const shapes = [new Circle(5), new Rectangle(4, 5)];

shapes.forEach((shape) => {
  console.log(shape.toString());
});

// Output:
// Area: 78.53981633974483
// Area: 20

Notice how both the Circle and Rectangle classes have a toString method (which we inherited from the Shape class - the parent class). However, the toString method in the Circle class overrides the toString method in the Shape class. This is an example of polymorphism and method overriding.

Object Freezing, Sealing, and Preventing Extensions

// Object.freeze() - Prevents adding, removing, or modifying properties
const frozenObj = Object.freeze({
  prop: 42,
});
frozenObj.prop = 33; // Fails silently in non-strict mode
console.log(frozenObj.prop); // 42

// Object.seal() - Prevents adding new properties and marking existing properties as non-configurable
const sealedObj = Object.seal({
  prop: 42,
});
sealedObj.prop = 33; // This works
sealedObj.newProp = "new"; // This fails silently in non-strict mode
console.log(sealedObj.prop); // 33
console.log(sealedObj.newProp); // undefined

// Object.preventExtensions() - Prevents adding new properties
const nonExtensibleObj = Object.preventExtensions({
  prop: 42,
});
nonExtensibleObj.prop = 33; // This works
nonExtensibleObj.newProp = "new"; // This fails silently in non-strict mode
console.log(nonExtensibleObj.prop); // 33
console.log(nonExtensibleObj.newProp); // undefined

These methods are useful for creating immutable objects or preventing accidental modifications to objects.

Best Practices for Writing Clean OOP Code in JavaScript

  1. Use ES6 Classes: They provide a cleaner, more intuitive syntax for creating objects and implementing inheritance.

  2. Follow the Single Responsibility Principle: Each class should have a single, well-defined purpose.

// Good ✅
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

class UserValidator {
  static validateEmail(email) {
    // Email validation logic
  }
}

// Not so good ❌
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  validateEmail() {
    // Email validation logic
  }
}
  1. Use Composition Over Inheritance: Favor object composition over class inheritance when designing larger systems.
// Composition
class Engine {
  start() {
    /* ... */
  }
}

class Car {
  constructor() {
    this.engine = new Engine();
  }

  start() {
    this.engine.start();
  }
}

// Inheritance
class Vehicle {
  start() {
    /* ... */
  }
}

class Car extends Vehicle {
  // ...
}
  1. Implement Private Fields: Use the latest JavaScript features or closures to create truly private fields.

  2. Use Getters and Setters: They provide more control over how properties are accessed and modified.

  3. Avoid Overusing this: Use object destructuring in methods to make the code cleaner and less prone to errors.

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  area() {
    const { width, height } = this;
    return width * height;
  }
}
  1. Use Method Chaining: It can make your code more readable and concise.
class Calculator {
  constructor() {
    this.value = 0;
  }

  add(n) {
    this.value  = n;
    return this;
  }

  subtract(n) {
    this.value -= n;
    return this;
  }

  result() {
    return this.value;
  }
}

const calc = new Calculator();
console.log(calc.add(5).subtract(2).result()); // 3
  1. Favor Declarative Over Imperative Programming: Use higher-order functions like map, filter, and reduce when working with collections.

  2. Use Static Methods Appropriately: Use static methods for utility functions that don't require access to instance-specific data.

  3. Write Self-Documenting Code: Use clear, descriptive names for classes, methods, and properties. Add comments only when necessary to explain complex logic.

Small Project: Building a Library Management System

Let's put our OOP knowledge into practice by building a simple Library Management System.

class Book {
  constructor(title, author, isbn) {
    this.title = title;
    this.author = author;
    this.isbn = isbn;
    this.isAvailable = true;
  }

  checkout() {
    if (this.isAvailable) {
      this.isAvailable = false;
      return true;
    }
    return false;
  }

  return() {
    this.isAvailable = true;
  }
}

class Library {
  constructor() {
    this.books = [];
  }

  addBook(book) {
    this.books.push(book);
  }

  findBookByISBN(isbn) {
    return this.books.find((book) => book.isbn === isbn);
  }

  checkoutBook(isbn) {
    const book = this.findBookByISBN(isbn);
    if (book) {
      return book.checkout();
    }
    return false;
  }

  returnBook(isbn) {
    const book = this.findBookByISBN(isbn);
    if (book) {
      book.return();
      return true;
    }
    return false;
  }

  get availableBooks() {
    return this.books.filter((book) => book.isAvailable);
  }
}

// Usage
const library = new Library();

library.addBook(
  new Book("The Great Gatsby", "F. Scott Fitzgerald", "9780743273565")
);
library.addBook(
  new Book("To Kill a Mockingbird", "Harper Lee", "9780446310789")
);

console.log(library.availableBooks.length); // 2

library.checkoutBook("9780743273565");

console.log(library.availableBooks.length); // 1

library.returnBook("9780743273565");

console.log(library.availableBooks.length); // 2

This project demonstrates the use of classes, encapsulation, methods, and properties in a real-world scenario.

Some Leetcode Problems on OOP

To further practice your OOP skills in JavaScript, try solving these problems:

  1. LeetCode: Design Parking System
  2. LeetCode: Design HashMap
  3. Codewars: Object Oriented Piracy

Conclusion

Object-Oriented Programming is a powerful paradigm that helps organize and structure code in a way that mirrors real-world objects and relationships. In this article, we've covered the fundamental concepts of OOP in JavaScript, from basic object creation to advanced topics like polymorphism and best practices.

Key takeaways:

  • OOP helps in creating modular, reusable, and maintainable code.
  • JavaScript provides multiple ways to implement OOP concepts, with ES6 classes offering a clean and intuitive syntax.
  • Principles like encapsulation, inheritance, polymorphism, and abstraction form the backbone of OOP.
  • Best practices, such as using composition over inheritance and following the single responsibility principle, can greatly improve code quality.

As you continue your journey with OOP in JavaScript, remember that practice is key. Try to apply these concepts in your projects, refactor existing code to follow OOP principles, and don't be afraid to explore advanced patterns and techniques.

References

For further reading and practice, check out these resources:

  1. MDN Web Docs: Object-oriented JavaScript
  2. JavaScript.info: Classes
  3. You Don't Know JS: this & Object Prototypes
  4. Eloquent JavaScript: Chapter 6: The Secret Life of Objects

Remember, mastering OOP is a journey. Keep coding, keep learning, and most importantly, enjoy the process of creating robust and elegant object-oriented JavaScript applications!



Stay Updated and Connected

To ensure you don't miss any part of this series and to connect with me for more in-depth discussions on Software Development (Web, Server, Mobile or Scraping / Automation), OOP, data structures and algorithms, and other exciting tech topics, follow me on:

  • GitHub
  • Linkedin
  • X (Twitter)

Stay tuned and happy coding ?‍??

リリースステートメント この記事は次の場所に転載されています: https://dev.to/emmanuelayinde/object-owned-programming-your-first-step-toward-mastering-dsa-16a3?1 侵害がある場合は、[email protected] までご連絡ください。それを削除するには
最新のチュートリアル もっと>

免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。

Copyright© 2022 湘ICP备2022001581号-3