Module: JavaScript
JavaScript·059·4 MIN READ

059: Prototypes, Prototypal Inheritance, and Classes

TOPICS COVERED: Prototypes, Prototypal Inheritance, and Classes

Outcomes

By the end of this lesson, you can:

  • explain prototype lookup;
  • distinguish an object's own properties from inherited properties;
  • use Object.getPrototypeOf() safely for inspection;
  • explain constructor functions and .prototype;
  • build classes with constructors, methods, inheritance, static methods, and private fields;
  • understand that JavaScript classes are built on the prototype system.

Mental Model: Objects Can Delegate Lookup

When JavaScript cannot find a property directly on an object, it may continue searching through that object's prototype chain.

js
const animal = {
  describe() {
    return `${this.name} makes a sound`;
  },
};

const dog = Object.create(animal);
dog.name = "Bruno";

console.log(dog.describe());

describe is not an own property of dog. It is found through the prototype chain.

Inspecting Ownership and Prototypes

js
console.log(Object.hasOwn(dog, "name"));     // true
console.log(Object.hasOwn(dog, "describe")); // false

console.log(Object.getPrototypeOf(dog) === animal); // true

Avoid using __proto__ in application code. Use standard APIs such as Object.getPrototypeOf() and Object.setPrototypeOf() when you genuinely need them.

Prototype Chain

text
dog
  ↓
animal
  ↓
Object.prototype
  ↓
null

Eventually lookup ends at null.

Constructor Functions

Before class syntax, constructor functions were a common pattern.

js
function Product(name, price) {
  this.name = name;
  this.price = price;
}

Product.prototype.getLabel = function () {
  return `${this.name} - ₹${this.price}`;
};

const tea = new Product("Tea", 20);
console.log(tea.getLabel());

The method is shared through Product.prototype instead of being recreated for every instance.

Class Syntax

js
class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }

  getLabel() {
    return `${this.name} - ₹${this.price}`;
  }
}

const tea = new Product("Tea", 20);

Class methods are still stored on the class's prototype.

js
console.log(
  Object.getPrototypeOf(tea) === Product.prototype
); // true

Inheritance

js
class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }

  getLabel() {
    return `${this.name} - ₹${this.price}`;
  }
}

class DiscountedProduct extends Product {
  constructor(name, price, discountPercent) {
    super(name, price);
    this.discountPercent = discountPercent;
  }

  getFinalPrice() {
    return this.price * (1 - this.discountPercent / 100);
  }
}

super() must run before using this in a derived constructor.

Method Overriding and super

js
class DiscountedProduct extends Product {
  getLabel() {
    return `${super.getLabel()} (${this.discountPercent}% off)`;
  }
}

Static Methods

Static methods belong to the class constructor, not instances.

js
class Money {
  static round(value) {
    return Math.round(value * 100) / 100;
  }
}

Money.round(12.345);

Use static methods for behavior associated with the abstraction but not with one specific instance.

Private Fields

js
class Wallet {
  #balance = 0;

  deposit(amount) {
    if (amount <= 0) {
      throw new Error("Amount must be positive");
    }

    this.#balance += amount;
  }

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

Private fields use # syntax and are enforced by the language.

Getters and Setters

js
class Order {
  constructor(items) {
    this.items = items;
  }

  get total() {
    return this.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
  }
}

Use accessors when property-like syntax genuinely improves the API. Avoid hiding expensive or surprising side effects behind a getter.

Composition versus Inheritance

Inheritance is not automatically the best reuse mechanism.

js
function createPricedProduct(product, pricingPolicy) {
  return {
    ...product,
    getPrice() {
      return pricingPolicy(product);
    },
  };
}

Composition can produce smaller, more flexible dependencies.

Choose based on the domain, not because one approach appears more "advanced."

Prototype Pollution Awareness

Objects that inherit from Object.prototype can be affected by unsafe merging patterns when attacker-controlled keys such as __proto__ are accepted.

Do not blindly copy untrusted properties into sensitive objects.

js
for (const [key, value] of Object.entries(untrusted)) {
  if (key === "__proto__" || key === "constructor" || key === "prototype") {
    continue;
  }

  target[key] = value;
}

Real applications should use well-reviewed libraries and robust validation rather than ad-hoc filters for security-sensitive merging.

Metaprogramming with Proxy and Reflect

Proxy can intercept fundamental object operations. Reflect exposes corresponding operations as functions.

js
const target = {
  price: 100,
};

const product = new Proxy(target, {
  get(object, property, receiver) {
    console.log("read:", property);

    return Reflect.get(
      object,
      property,
      receiver
    );
  },

  set(object, property, value, receiver) {
    if (property === "price" && value < 0) {
      throw new RangeError("Price cannot be negative");
    }

    return Reflect.set(
      object,
      property,
      value,
      receiver
    );
  },
});

console.log(product.price);
product.price = 120;

A proxy can trap operations such as:

  • property reads/writes;
  • in checks;
  • deletion;
  • enumeration-related behavior;
  • function calls;
  • construction.

This power comes with complexity.

Do not use a proxy merely to avoid writing explicit application methods. Hidden interception can make debugging, identity assumptions, private fields, and performance harder to reason about.

Why Reflect

Inside a trap, Reflect usually expresses the default operation more accurately than manually reconstructing it.

js
const logging = new Proxy(target, {
  has(object, property) {
    console.log("checking", property);
    return Reflect.has(object, property);
  },
});

Reflect is also useful on its own for dynamic operations:

js
Reflect.get(product, "price");
Reflect.set(product, "price", 140);
Reflect.ownKeys(product);

For ordinary CRUD/domain code, normal property access is clearer. Learn proxies because frameworks, reactivity systems, validation layers, mocks, and advanced libraries may use them.

Prototype Debugging Workflow

When a property result surprises you:

js
console.log(Object.hasOwn(object, "status"));
console.log(Object.getPrototypeOf(object));
console.log(
  Object.getOwnPropertyDescriptor(
    object,
    "status"
  )
);

Then walk upward deliberately:

js
let current = object;

while (current !== null) {
  console.log(current);
  current = Object.getPrototypeOf(current);
}

This is more reliable than guessing whether the value came from the instance, class prototype, parent prototype, or Object.prototype.

Worked Example: Order Hierarchy

js
class Order {
  constructor(id, items = []) {
    this.id = id;
    this.items = items;
  }

  addItem(item) {
    this.items.push(item);
  }

  get total() {
    return this.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
  }
}

class DeliveryOrder extends Order {
  constructor(id, items, deliveryFee) {
    super(id, items);
    this.deliveryFee = deliveryFee;
  }

  get total() {
    return super.total + this.deliveryFee;
  }
}

const order = new DeliveryOrder(
  "ORD-1",
  [{ price: 100, quantity: 2 }],
  30
);

console.log(order.total); // 230

Advanced Notes: Property Descriptors and Prototype-Safe APIs

Every ordinary property has descriptor metadata.

js
const product = {};

Object.defineProperty(product, "sku", {
  value: "TEA-001",
  writable: false,
  enumerable: true,
  configurable: false,
});

console.log(
  Object.getOwnPropertyDescriptor(product, "sku")
);

Descriptors control:

  • value or getter/setter behavior;
  • writability;
  • enumerability;
  • configurability.

Most application code should use normal property syntax, but descriptors explain how many built-in and framework behaviors work.

Prototype methods versus instance fields

js
class Counter {
  increment() {
    // one shared prototype method
  }

  reset = () => {
    // typically one function per instance
  };
}

Instance arrow fields can solve callback binding problems, but they have different memory/identity characteristics from prototype methods. Use them intentionally.

instanceof

js
const product = new Product("Tea", 20);

console.log(product instanceof Product); // true

instanceof follows the prototype chain. It can become unreliable across realms (for example, certain iframe boundaries) and is not a substitute for validating external data.

Factory alternative

js
function createProduct(name, price) {
  return {
    name,
    price,
    getLabel() {
      return `${name} - ₹${price}`;
    },
  };
}

Factories, classes, and plain objects are all valid tools. Choose the simplest model that expresses ownership, lifecycle, and behavior clearly.

Mistakes and Debugging

  • assuming classes replace prototypes;
  • defining shared methods inside constructors unnecessarily;
  • using inheritance where composition would be simpler;
  • forgetting super() in derived constructors;
  • relying on inherited properties when you need own-property checks;
  • mutating prototypes at runtime without a strong reason.

Best Practices

  • Understand prototypes even if you mostly write classes.
  • Prefer clear, shallow inheritance trees.
  • Use Object.hasOwn() for ownership checks.
  • Keep constructors focused on valid initialization.
  • Favor composition when behavior needs to vary independently.
  • Treat untrusted object merging as a security boundary.

Exercises

Core

Create an object with Object.create() and prove where a method is found.

Practice

Build Product and DiscountedProduct classes with a computed final price.

Professional Extension

Refactor the inheritance example into composition and compare the trade-offs.

Recap

JavaScript's inheritance model is prototype-based. class is a more structured syntax on top of that model, not a separate object system.