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

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

  • A constructor function is intended to be used with the new keyword to create new objects.
  • When called with new, a constructor function creates a new object, sets this to that object, and returns it.
  • Regular functions are called without new and this depends on how the function is called.

How does inheritance work in ES6 classes?

  • Inheritance is achieved using the extends keyword.
  • A subclass extends a parent class and inherits its properties and methods.
  • The super() function is used within the subclass constructor to call the parent class constructor.
ES6 Classes AdvantagesDifficulty: Easy

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

  • Cleaner and more concise syntax.
  • Easier to read and maintain.
  • Simplifies inheritance with extends and super.
  • Supports static methods, getters, setters, and private fields.

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

  • super() calls the constructor of the parent class.
  • It is necessary to use super() before accessing this in a subclass constructor.
  • Allows access to parent class methods and properties.

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

  • Use the # prefix to declare private fields.

  • Private fields are only accessible within the class body.

  • Example:

    class Example {
      #privateField = 'secret'
    
      getSecret() {
        return this.#privateField
      }
    }
    

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