Object Creation and Class Patterns in JavaScript

Objects are a fundamental part of JavaScript, providing a way to store and manipulate data in a structured manner. JavaScript is a prototype-based language, but with the introduction of ES6 classes, it has become easier to implement object-oriented programming patterns. In this lesson, we will explore different methods of creating objects, understand constructor functions, and learn about ES6 classes and inheritance.

Object Creation in JavaScript

Object Literals

  • Definition: The simplest way to create an object using curly braces {}.

Example:

const person = {
  name: 'Alice',
  age: 30,
  greet: function () {
    console.log(`Hello, my name is ${this.name}.`)
  },
}

person.greet() // Outputs: Hello, my name is Alice.

Explanation:

  • The person object has properties name, age, and a method greet.

Using Object.create()

  • Definition: Creates a new object with the specified prototype object and properties.

Example:

const animal = {
  eat: function () {
    console.log(`${this.name} is eating.`)
  },
}

const dog = Object.create(animal)
dog.name = 'Buddy'
dog.eat() // Outputs: Buddy is eating.

Explanation:

  • dog is created with animal as its prototype.

Constructor Functions

  • Definition: Functions used to create multiple similar objects.

Syntax:

function ConstructorFunction(parameters) {
  // Initialization code
}

Example:

function Car(make, model) {
  this.make = make
  this.model = model
  this.getInfo = function () {
    return `${this.make} ${this.model}`
  }
}

const myCar = new Car('Toyota', 'Corolla')
console.log(myCar.getInfo()) // Outputs: Toyota Corolla

Explanation:

  • Car is a constructor function.
  • The new keyword creates a new object, sets this to that object, and returns it.

ES6 Classes

  • Definition: Syntactic sugar over JavaScript's existing prototype-based inheritance.

Syntax:

class ClassName {
  constructor(parameters) {
    // Initialization code
  }

  // Methods
}

Example:

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

  greet() {
    console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`)
  }
}

const bob = new Person('Bob', 25)
bob.greet() // Outputs: Hi, I'm Bob and I'm 25 years old.

Explanation:

  • Person is a class with a constructor and a method greet.

Constructor Functions in Detail

Understanding Constructor Functions

  • Purpose: To create multiple objects with the same properties and methods.
  • Convention: Constructor function names start with an uppercase letter.

How it works:

  • When a function is invoked with new:
    • A new object is created.
    • The this keyword is set to the new object.
    • The new object inherits from the constructor's prototype.
    • The constructor function code runs, potentially modifying this.
    • The new object is returned unless the constructor returns a non-primitive value.

Example:

function Circle(radius) {
  this.radius = radius
}

Circle.prototype.area = function () {
  return Math.PI * this.radius * this.radius
}

const circle = new Circle(5)
console.log(circle.area()) // Outputs: 78.53981633974483

Explanation:

  • The Circle constructor initializes the radius.
  • The area method is added to Circle.prototype, so all instances share the same method.

Best Practices with Constructor Functions

  • Always use the new keyword when calling a constructor function.
  • Define shared methods on the constructor's prototype, not within the constructor.
  • Use this to assign properties within the constructor.

Avoiding Errors:

  • Forgetting new can lead to unexpected results, as this would refer to the global object in non-strict mode or undefined in strict mode.

Example without new:

const circle = Circle(5) // Forgot 'new'
console.log(radius) // Outputs: 5 (in global scope)

Explanation:

  • Without new, this.radius assigns radius to the global scope.

Solution:

  • Enforce the use of new:
function Circle(radius) {
  if (!(this instanceof Circle)) {
    return new Circle(radius)
  }
  this.radius = radius
}

ES6 Classes and Inheritance

Defining Classes

  • Classes are defined using the class keyword.

Syntax:

class ClassName {
  constructor(parameters) {
    // Initialization code
  }

  method1() {
    // Method code
  }

  method2() {
    // Method code
  }

  // Static methods
  static staticMethod() {
    // Static method code
  }
}

Example:

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

  getArea() {
    return this.width * this.height
  }
}

const rect = new Rectangle(10, 5)
console.log(rect.getArea()) // Outputs: 50

Explanation:

  • Rectangle class has a constructor and a method getArea.

Class Inheritance with extends

  • Use extends to create a subclass that inherits from a parent class.
  • Use super() to call the parent class's constructor.

Example:

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

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

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

  speak() {
    console.log(`${this.name} barks.`)
  }
}

const dog = new Dog('Buddy', 'Golden Retriever')
dog.speak() // Outputs: Buddy barks.

Explanation:

  • Dog extends Animal.
  • The speak method in Dog overrides the one in Animal.

Static Methods

  • Defined using the static keyword.
  • Called on the class itself, not on instances.

Example:

class MathUtils {
  static add(a, b) {
    return a + b
  }
}

console.log(MathUtils.add(2, 3)) // Outputs: 5

Explanation:

  • add is a static method and is called directly on MathUtils.

Getters and Setters

  • Use get and set to define getters and setters.

Example:

class Person {
  constructor(name) {
    this._name = name
  }

  get name() {
    return this._name.toUpperCase()
  }

  set name(newName) {
    this._name = newName
  }
}

const person = new Person('Charlie')
console.log(person.name) // Outputs: CHARLIE
person.name = 'Dave'
console.log(person.name) // Outputs: DAVE

Explanation:

  • The getter name returns the name in uppercase.
  • The setter name allows updating the name.

Private Fields (ES2021)

  • Use # prefix to declare private fields.

Example:

class Counter {
  #count = 0 // Private field

  increment() {
    this.#count++
    console.log(`Count: ${this.#count}`)
  }
}

const counter = new Counter()
counter.increment() // Outputs: Count: 1
// console.log(counter.#count); // SyntaxError: Private field '#count' must be declared in an enclosing class

Explanation:

  • #count is not accessible outside the class.

Combining Constructor Functions and Prototypes

  • Before ES6 classes, inheritance was achieved using constructor functions and manipulating prototypes.

Example:

function Vehicle(make) {
  this.make = make
}

Vehicle.prototype.drive = function () {
  console.log(`${this.make} is driving.`)
}

function Car(make, model) {
  Vehicle.call(this, make) // Inherit properties
  this.model = model
}

Car.prototype = Object.create(Vehicle.prototype) // Inherit methods
Car.prototype.constructor = Car // Set constructor reference

Car.prototype.honk = function () {
  console.log(`${this.make} ${this.model} says beep beep!`)
}

const myCar = new Car('Toyota', 'Corolla')
myCar.drive() // Outputs: Toyota is driving.
myCar.honk() // Outputs: Toyota Corolla says beep beep!

Explanation:

  • Car inherits from Vehicle both properties and methods.

Advantages of ES6 Classes

  • Cleaner Syntax: More readable and familiar to those from class-based OOP languages.
  • Inheritance Simplification: Easier to implement inheritance with extends and super.
  • Static Methods: Conveniently define methods on the class.
  • Getters and Setters: Easily define property accessors.
  • Private Fields: With the introduction of private fields, data encapsulation is improved.

Best Practices

Use ES6 Classes for Object-Oriented Programming

  • Reason: Cleaner syntax, better readability, and modern features.

Name Classes and Constructors with UpperCamelCase

  • Convention: Helps differentiate constructors and classes from regular functions.

Example:

class UserAccount {
  // ...
}

Use super() in Subclasses

  • Reason: Ensure the parent class's constructor is called.

Define Methods on Prototypes

  • For constructor functions, define shared methods on ConstructorFunction.prototype.

Avoid Extending Built-in Objects

  • Reason: Can lead to unexpected behavior and conflicts.

Be Mindful of this Binding

  • In classes and constructor functions, this refers to the instance.
  • Arrow functions do not have their own this; be cautious when using them as methods.

Exercises

Exercise 1: Constructor Functions

Question:

Create a constructor function Book that takes title and author as parameters. Add a method getDetails to the prototype that returns a string with the book's details.

Answer:

function Book(title, author) {
  this.title = title
  this.author = author
}

Book.prototype.getDetails = function () {
  return `${this.title} by ${this.author}`
}

const book = new Book('1984', 'George Orwell')
console.log(book.getDetails()) // Outputs: 1984 by George Orwell

Exercise 2: ES6 Class Inheritance

Question:

Create a class Shape with a method area() that returns 0. Then, create a subclass Square that extends Shape and overrides the area() method to calculate the area of the square.

Answer:

class Shape {
  area() {
    return 0
  }
}

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

  area() {
    return this.sideLength * this.sideLength
  }
}

const square = new Square(5)
console.log(square.area()) // Outputs: 25

Exercise 3: Using super()

Question:

Explain why the following code results in an error and fix it.

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

class Cat extends Animal {
  constructor(name) {
    this.type = 'Cat'
    this.name = name
  }
}

const kitty = new Cat('Whiskers')
console.log(kitty.name)

Answer:

Error:

  • ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

Explanation:

  • In a subclass constructor, you must call super() before accessing this.

Fixed Code:

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

class Cat extends Animal {
  constructor(name) {
    super(name)
    this.type = 'Cat'
  }
}

const kitty = new Cat('Whiskers')
console.log(kitty.name) // Outputs: Whiskers

Exercise 4: Private Fields

Question:

Implement a class BankAccount with a private field #balance. Provide methods to deposit, withdraw, and getBalance, ensuring that the balance cannot be accessed or modified directly from outside the class.

Answer:

class BankAccount {
  #balance = 0

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

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount
      console.log(`Withdrew: $${amount}`)
    } else {
      console.log('Insufficient funds')
    }
  }

  getBalance() {
    console.log(`Balance: $${this.#balance}`)
  }
}

const account = new BankAccount()
account.deposit(100) // Outputs: Deposited: $100
account.withdraw(30) // Outputs: Withdrew: $30
account.getBalance() // Outputs: Balance: $70
// console.log(account.#balance); // SyntaxError

Exercise 5: Static Methods

Question:

Create a class Calculator with static methods add, subtract, multiply, and divide. These methods should perform the respective arithmetic operations.

Answer:

class Calculator {
  static add(a, b) {
    return a + b
  }

  static subtract(a, b) {
    return a - b
  }

  static multiply(a, b) {
    return a * b
  }

  static divide(a, b) {
    if (b !== 0) {
      return a / b
    } else {
      throw new Error('Division by zero')
    }
  }
}

console.log(Calculator.add(2, 3)) // Outputs: 5
console.log(Calculator.subtract(5, 2)) // Outputs: 3
console.log(Calculator.multiply(3, 4)) // Outputs: 12
console.log(Calculator.divide(10, 2)) // Outputs: 5

Understanding the various ways to create and work with objects in JavaScript, from constructor functions to modern ES6 classes, is essential for writing well-structured and maintainable code. By mastering these patterns and their nuances, you'll be better equipped to implement object-oriented programming concepts effectively and handle related technical interview questions with confidence.

Practice Problems

How does inheritance work in ES6 classes?

Loading...

What is the difference between a constructor function and a regular function in JavaScript?

Loading...

ES6 Classes AdvantagesDifficulty: Easy

What are the advantages of using ES6 classes over constructor functions?

Loading...

Explain the role of `super()` in class inheritance.

Loading...

Can you explain how to implement private variables in ES6 classes?

Loading...

Let's continue exploring the next page. Take your time, and proceed when you're ready.

Lesson completed?

Found a bug, typo, or have feedback?

Let me know