Practice Questions

1. Write a Rectangle class with width and height properties, and methods getArea() and getPerimeter(). Instantiate it and call both methods.

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

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

  getPerimeter() {
    return 2 * (this.width + this.height);
  }
}

const rect = new Rectangle(5, 3);
console.log(rect.getArea());  // 15
console.log(rect.getPerimeter());  // 16

2. What is the difference between call, apply, and bind? Show an example of each.

Solution
  • call() invokes a function immediately with a specified this value. Arguments are passed individually.
  • apply() invokes a function immediately with a specified this value. Arguments are passed as an array.
  • bind() returns a new function with this permanently set. It does not invoke the function immediately.
function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: "Alice" };

// call - arguments passed individually
greet.call(person, "Hello", "!");  // Hello, Alice!

// apply - arguments passed as array
greet.apply(person, ["Hi", "?"]);  // Hi, Alice?

// bind - returns a new function
const boundGreet = greet.bind(person);
boundGreet("Hey", ".");  // Hey, Alice.

3. How do arrow functions differ from regular functions regarding this? Why does this matter for callbacks?

Solution

Arrow functions do not have their own this binding. Instead, they capture this from the enclosing lexical scope at the time they are defined. Regular functions get their this based on how they are called.

This matters for callbacks because regular function callbacks lose the surrounding context:

const account = {
  balance: 100,
  displayWithRegular: function () {
    setTimeout(function () {
      console.log(this.balance);  // undefined - 'this' is not the account
    }, 100);
  },
  displayWithArrow: function () {
    setTimeout(() => {
      console.log(this.balance);  // 100 - 'this' captured from enclosing method
    }, 100);
  },
};

With an arrow function you do not need a workaround like const self = this.

4. Write a BankAccount class with deposit(amount), withdraw(amount), and getBalance() methods. The balance should not be directly accessible from outside the class. What JavaScript feature makes this possible?

Solution
class BankAccount {
  #balance = 0;

  constructor(initialBalance = 0) {
    this.#balance = initialBalance;
  }

  deposit(amount) {
    this.#balance += amount;
  }

  withdraw(amount) {
    if (amount <= this.#balance) {
      this.#balance -= amount;
    }
  }

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

const account = new BankAccount(100);
account.deposit(50);
account.withdraw(30);
console.log(account.getBalance());  // 120
console.log(account.balance);  // undefined (not accessible)
// console.log(account.#balance);  // SyntaxError

5. Create a Person class with firstName and lastName properties. Add a getter fullName that returns the full name, and a setter fullName that splits a full name into first and last names.

Solution
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  set fullName(value) {
    const parts = value.split(" ");
    this.firstName = parts[0];
    this.lastName = parts[1];
  }
}

const person = new Person("John", "Doe");
console.log(person.fullName); // John Doe

person.fullName = "Jane Smith";
console.log(person.firstName); // Jane
console.log(person.lastName); // Smith

Note: getters and setters are accessed like properties (no parentheses), but they act like methods.

6. What is the difference between static and instance members? Write a Counter class with a static field that tracks how many instances have been created.

Solution
  • Instance members belong to each object created from the class. Each instance has its own copy.
  • Static members belong to the class itself. They are shared across all instances and accessed via the class name.
class Counter {
  static instanceCount = 0;

  constructor() {
    Counter.instanceCount++;
  }

  static getInstanceCount() {
    return Counter.instanceCount;
  }
}

const c1 = new Counter();
const c2 = new Counter();
const c3 = new Counter();

console.log(Counter.instanceCount); // 3
console.log(Counter.getInstanceCount()); // 3
// console.log(c1.instanceCount); // undefined (not an instance member)

7. Create a Vehicle class with a start() method. Then create a Car class that extends Vehicle and adds a drive() method. Show how to use super to call the parent constructor.

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

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

class Car extends Vehicle {
  constructor(make, model, numDoors) {
    super(make, model); // Call parent constructor
    this.numDoors = numDoors;
  }

  drive() {
    console.log(`${this.make} ${this.model} is driving`);
  }
}

const myCar = new Car("Toyota", "Camry", 4);
myCar.start();  // Toyota Camry is starting
myCar.drive();  // Toyota Camry is driving
console.log(myCar instanceof Vehicle);  // true
console.log(myCar instanceof Car);  // true

8. Create a custom ValidationError class that extends the built-in Error class. Use it in a function that throws this error when given invalid input.

Solution
class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

function validateAge(age) {
  if (typeof age !== "number") {
    throw new ValidationError("Age must be a number");
  }
  if (age < 0 || age > 150) {
    throw new ValidationError("Age must be between 0 and 150");
  }
  return true;
}

try {
  validateAge(-5);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error("Validation failed:", error.message);
  } else {
    throw error;
  }
}
// Output: Validation failed: Age must be between 0 and 150

9. What is method overriding? Write a Shape class with a describe() method, then create Circle and Square subclasses that override describe() with their own implementations.

Solution

Method overriding allows a subclass to provide a different implementation of a method that is already defined in its parent class. When the method is called, the subclass version is executed (dynamic dispatch).

class Shape {
  describe() {
    console.log("I am a shape");
  }
}

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

  describe() {
    console.log(`I am a circle with radius ${this.radius}`);
  }
}

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

  describe() {
    console.log(`I am a square with side ${this.side}`);
  }
}

const shapes = [new Shape(), new Circle(5), new Square(4)];
shapes.forEach((shape) => shape.describe());
// I am a shape
// I am a circle with radius 5
// I am a square with side 4

10. A TechLead needs to both manage people and write code. You have existing Manager and Developer classes. Explain why using inheritance here is problematic and demonstrate how composition provides a solution.

Solution
  • Inheritance creates an “is-a” relationship: a GradStudent is a Student. The subclass inherits all properties and methods from the parent.
  • Composition creates a “has-a” relationship: a Car has an Engine. Objects are built by combining smaller, focused pieces.

Prefer composition when:

  • You need to combine behaviors from multiple sources (JavaScript does not support multiple inheritance)
  • The relationships might change over time
  • You want loose coupling between components
  • You are modeling “has-a” or “uses-a” relationships

Prefer inheritance when:

  • There is a clear, stable “is-a” relationship
  • You want to leverage polymorphism
  • The hierarchy is shallow (one or two levels)

Example of composition:

const canFly = (state) => ({
  fly: () => console.log(`${state.name} is flying`),
});

const canSwim = (state) => ({
  swim: () => console.log(`${state.name} is swimming`),
});

function createDuck(name) {
  const state = { name };
  return { ...state, ...canFly(state), ...canSwim(state) };
}

const duck = createDuck("Donald");
duck.fly(); // Donald is flying
duck.swim(); // Donald is swimming

11. Rewrite this constructor function and prototype-based code using ES6 class syntax:

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

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

Book.prototype.setTitle = function (newTitle) {
  this.title = newTitle;
};
Solution
class Book {
  constructor(title, author) {
    this.title = title;
    this.author = author;
  }

  getSummary() {
    return `${this.title} by ${this.author}`;
  }

  setTitle(newTitle) {
    this.title = newTitle;
  }
}

const book = new Book("1984", "George Orwell");
console.log(book.getSummary()); // 1984 by George Orwell
book.setTitle("Animal Farm");
console.log(book.getSummary()); // Animal Farm by George Orwell

The class syntax is syntactic sugar over the constructor function and prototype pattern. Under the hood, JavaScript still uses prototypes.

12. What is the advantage of attaching methods to a constructor’s prototype instead of defining them inside the constructor? Demonstrate with code.

Solution

When methods are defined inside the constructor, each instance gets its own copy of the method, using more memory. When methods are attached to the prototype, all instances share the same method, which is more memory-efficient.

// Each instance has its own copy of deposit
function AccountBad() {
  this.balance = 0;
  this.deposit = function (amount) {
    this.balance += amount;
  };
}

// All instances share the same deposit method
function AccountGood() {
  this.balance = 0;
}
AccountGood.prototype.deposit = function (amount) {
  this.balance += amount;
};

const bad1 = new AccountBad();
const bad2 = new AccountBad();
console.log(bad1.deposit === bad2.deposit);  // false (different copies)

const good1 = new AccountGood();
const good2 = new AccountGood();
console.log(good1.deposit === good2.deposit);  // true (shared method)

This is why ES6 classes define methods on the prototype by default when you declare them in the class body.