› Module 8: Object-Oriented JavaScript › 8.1 Prototypes & the Prototype Chain
M O D U L E

Module 8: Object-Oriented JavaScript

How JavaScript models 'is-a' relationships

8.1 Prototypes & the Prototype Chain

Unlike languages like Java or C++ that feature classical class-based inheritance where blueprints are copied into new instances, JavaScript implements prototypal inheritance. Every JavaScript object possesses an internal hidden link (known in specifications as [[Prototype]] and exposed in browser runtimes via __proto__) pointing to another object from which it inherits properties and methods.

Figure 8.1: Prototypal Inheritance & Internal Prototype Chain Resolution dog (Instance Object) name: "Rex" bark() not found here → checks __proto__ __proto__ Dog.prototype bark() ✓ FOUND! Executes with this = dog __proto__ → Object.prototype __proto__ Object.prototype toString(), valueOf() hasOwnProperty() __proto__ → null (End)

Figure 8.1: When evaluating dog.bark(), the engine checks dog, traverses __proto__ to Dog.prototype, executes the method, and terminates at null.

1. How Property Lookup Works

When you access any property or method obj.property, the V8 engine executes the following recursive algorithm:

  1. Check if the property exists directly on obj as an own property. If found, return its value immediately.
  2. If not found, inspect Object.getPrototypeOf(obj) (the prototype linked via __proto__).
  3. Repeat step 1 on the prototype object. Continue ascending the chain until the property is found or until Object.prototype.__proto__ resolves to null.
  4. If the end of the chain is reached without finding the property, evaluate to undefined.

2. Property Shadowing

Assigning a value to an object property (dog.bark = customBark) does NOT overwrite the method on Dog.prototype. Instead, it creates an own property directly on dog, effectively shadowing (masking) the prototype method for that single instance.

3. Function.prototype vs. Object.__proto__

A classic JavaScript interview distinction:

  • ConstructorFunction.prototype: The template object attached to a constructor function that will become the [[Prototype]] for all instances created via new ConstructorFunction().
  • instance.__proto__ (or Object.getPrototypeOf(instance)): The actual link on an instance pointing up to its prototype.
// 1. Classical Prototypal Model with Constructor Functions
function Dog(name, breed) {
  this.name = name;
  this.breed = breed;
}

// Attach shared method to prototype (Shared in memory across all instances!)
Dog.prototype.bark = function() {
  console.log(this.name + " says Woof!");
};

const rex = new Dog("Rex", "German Shepherd");
rex.bark(); // "Rex says Woof!"

// Verifying prototype links:
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null);          // true (Terminal node)

// Checking Own vs Prototype properties:
console.log(Object.hasOwn(rex, "name")); // true (Own property)
console.log(Object.hasOwn(rex, "bark")); // false (Inherited from prototype!)

8.2 ES6 Classes

Introduced in ECMAScript 2015 (ES6), the class keyword provides clean, declarative syntactic sugar over JavaScript's existing prototype system. It offers a standardized syntax that developers accustomed to Java, C++, or Python find familiar, while maintaining JavaScript's dynamic prototypal foundation.

Figure 8.2: ES6 Class Architecture in Memory (Constructor, Prototype & Private Fields) User (Constructor Function) constructor(name, email) Initializes instance state static compare(u1, u2) Static utility on class itself .prototype User.prototype (Shared) login() { ... } Shared by all instances get role() / set role() Getter & Setter methods __proto__ const u1 = new User() name: "Sam" email: "sam@dev" #passwordHash Private Field (ES2022)

Figure 8.2: ES6 classes map to a constructor function, a shared prototype holding methods, and instance objects with optional private #fields.

1. Core Features of ES6 Classes

  • Constructor: Special method invoked during new ClassName() to instantiate state. Only one constructor is permitted per class.
  • Instance Methods: Defined cleanly in the class body without the function keyword. Automatically attached to ClassName.prototype.
  • Static Methods & Fields: Prefixed with static. Called directly on the class identifier (e.g. User.validate()). They cannot access instance properties via this.
  • Getters and Setters: Use get and set keywords to bind object properties to functions, enabling validation and computed attributes.
  • Private Fields (ES2022): Prefixed with a hash symbol #. They are completely inaccessible from outside the class instance — offering true hard encapsulation enforced by the runtime.

2. Classes vs. Functions: Key Differences

  1. No Hoisting: Class declarations are not hoisted like regular functions; accessing a class before declaration throws a ReferenceError (Temporal Dead Zone).
  2. Strict Mode: All code inside a class body automatically runs in 'use strict' mode.
  3. Mandatory new: Calling a class without the new operator immediately throws a TypeError: Class constructor cannot be invoked without 'new'.
class Account {
  // 1. Private fields (Encapsulated in memory)
  #balance;
  #accountNumber;

  // 2. Static class property
  static totalAccountsCreated = 0;

  constructor(accountNumber, initialDeposit = 0) {
    this.#accountNumber = accountNumber;
    this.#balance = initialDeposit;
    Account.totalAccountsCreated++;
  }

  // 3. Getter for computed / controlled read access
  get balance() {
    return "$" + this.#balance.toFixed(2);
  }

  // 4. Public instance method
  deposit(amount) {
    if (amount <= 0) throw new Error("Deposit must be positive");
    this.#balance += amount;
    return this.#balance;
  }

  // 5. Static factory method
  static createZeroBalance(accountNumber) {
    return new Account(accountNumber, 0);
  }
}

const myAcc = new Account("ACT-1002", 500);
myAcc.deposit(250);
console.log(myAcc.balance); // "$750.00"
// console.log(myAcc.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class

8.3 Class Inheritance

Subclassing in JavaScript is implemented via the extends keyword, allowing a child class to inherit properties, methods, and static utilities from a base parent class. Inside a derived class constructor, the super() operator must be invoked to initialize the parent instance state and bind the this context.

Figure 8.3: ES6 Class Inheritance (extends and super) Dual Prototype Linkage class Vehicle (Base Class) move() • static identify() extends super() class ElectricCar extends Vehicle super(brand, speed); // Initializes 'this' move() → Overrides parent method Dual Chain Linkage Rules 1. Car.prototype.__proto__ = Vehicle.prototype 2. Car.__proto__ = Vehicle (Static methods!)

Figure 8.3: extends links both the instance prototype chain AND the constructor function chain, enabling static and instance method inheritance.

1. The Golden Rule of super()

In a derived subclass constructor, you must invoke super() before accessing this. In JavaScript, derived classes do not allocate memory for this themselves; the base parent class constructor is responsible for allocating and binding the initial instance object. Attempting to read or write this.property before calling super() throws a ReferenceError: Must call super constructor in derived class before accessing 'this'.

2. Method Overriding & Polymorphism

A child class can provide a specialized implementation of an inherited method. If the child wishes to augment rather than completely replace the parent logic, it can invoke super.methodName() to reuse the base implementation.

3. Static Method Inheritance

A unique feature of JavaScript class inheritance: static methods declared on the parent class are automatically inherited by the subclass because Subclass.__proto__ === ParentClass. This allows calling Subclass.parentStaticMethod() directly.

class Vehicle {
  constructor(brand, speed) {
    this.brand = brand;
    this.speed = speed;
  }

  move() {
    console.log(this.brand + " travels at " + this.speed + " km/h.");
  }

  static compareSpeed(v1, v2) {
    return v1.speed - v2.speed;
  }
}

class ElectricCar extends Vehicle {
  constructor(brand, speed, batteryCapacity) {
    super(brand, speed); // MUST be first: allocates 'this'
    this.batteryCapacity = batteryCapacity;
  }

  // Method overriding with super delegation:
  move() {
    super.move(); // Reuses base logic
    console.log("Running silently on " + this.batteryCapacity + " kWh battery.");
  }
}

const tesla = new ElectricCar("Tesla Model 3", 200, 75);
tesla.move();
// Output:
// "Tesla Model 3 travels at 200 km/h."
// "Running silently on 75 kWh battery."

// Static methods are inherited by subclasses:
console.log(ElectricCar.compareSpeed(tesla, { speed: 180 })); // 20
MODULE COMPREHENSIVE QUIZ

Module 8 Final Quiz

Test your understanding across all 3 topics of this module with interview-grade questions, tricky edge cases, and code output predictions.

Take Module 8 Final Quiz →
Yukti Mascot