HomeCurriculumJavaScript BasicsObjects, Prototypes & Prototypal Inheritance
Objects, Prototypes & Prototypal Inheritance Mechanics

Objects, Prototypes & Prototypal Inheritance

Master prototypal delegation, the `[[Prototype]]` internal slot, prototype chain lookup traversal, property attribute descriptors, and ES6 class syntax desugaring.

DT
Authored by DevThesis Systems Editorial Team
Reviewed for Technical Accuracy & Code Determinism

1. Object Anatomy, Property Descriptors & Property Attributes#

In JavaScript, an Object is an unordered collection of properties, where each property consists of a key (either a String or a Symbol) and an associated set of Property Attributes. Rather than merely storing a value, the JavaScript runtime maintains a formal internal record known as a Property Descriptor that dictates how the property behaves under mutation, iteration, and deletion.

text
+-------------------------------------------------------------------------+
|                  THE JAVASCRIPT PROPERTY DESCRIPTOR TAXONOMY            |
+-----------------------------------+-------------------------------------+
| DATA DESCRIPTOR                   | ACCESSOR DESCRIPTOR                 |
+-----------------------------------+-------------------------------------+
| - value: Any JavaScript value     | - get: Function returning value     |
| - writable: Boolean (Can mutate)  | - set: Function accepting new value |
| - enumerable: Boolean (In for..in)| - enumerable: Boolean (In for..in)  |
| - configurable: Boolean (Can delete/modify descriptor attributes)        |
+-----------------------------------+-------------------------------------+

The Four Property Attributes Explained#

  1. `value`: The actual data stored at the property key (e.g., 42, "admin", <Function>).
  2. `writable`: If true, the property's value can be changed using an assignment operator (obj.x = 99). If false, assignment fails silently in loose mode and throws a TypeError in strict mode.
  3. `enumerable`: If true, the property appears when iterating over the object using for...in loops or Object.keys(). If false, the property is hidden from enumeration (though still accessible via direct lookup or Object.getOwnPropertyNames()).
  4. `configurable`: Controls the metadata lifecycle:

- If true, the property can be deleted via the delete operator, and its descriptor attributes can be modified between Data and Accessor descriptors.

- If false, the property cannot be deleted, and its attributes cannot be altered (except changing writable from true to false).

Inspecting and Defining Descriptors#

javascript
const serverConfig = {};

// Defining property with explicit attributes
Object.defineProperty(serverConfig, "port", {
  value: 8080,
  writable: false,      // Read-only
  enumerable: true,     // Visible in Object.keys()
  configurable: false   // Cannot be deleted or reconfigured
});

console.log(serverConfig.port); // 8080
// serverConfig.port = 3000;    // Throws TypeError in strict mode!
// delete serverConfig.port;    // Throws TypeError in strict mode!

// Inspecting descriptor metadata
const descriptor = Object.getOwnPropertyDescriptor(serverConfig, "port");
console.log(descriptor);
// { value: 8080, writable: false, enumerable: true, configurable: false }

Object Tamper-Proofing Tiers: Extensibility, Sealing, and Freezing#

JavaScript provides three progressive levels of object immutability:

text
+-------------------------------------------------------------------------+
|                  OBJECT TAMPER-PROOFING LEVEL MATRIX                    |
+---------------------+-------------------+---------------+---------------+
| METHOD              | ADD PROPERTIES?   | DELETE PROPS? | MUTATE VALUE? |
+---------------------+-------------------+---------------+---------------+
| Object.             | NO                | YES           | YES           |
| preventExtensions() |                   |               |               |
+---------------------+-------------------+---------------+---------------+
| Object.seal()       | NO                | NO            | YES           |
|                     |                   | (Config=false)|               |
+---------------------+-------------------+---------------+---------------+
| Object.freeze()     | NO                | NO            | NO            |
|                     |                   | (Config=false)| (Write=false) |
+---------------------+-------------------+---------------+---------------+

2. The Prototype Architecture: `[[Prototype]]`, `__proto__`, and `.prototype`#

JavaScript does not implement classical class-based inheritance like Java, C++, or C#. Instead, JavaScript implements Prototypal Delegation. Every object in JavaScript has an internal link to another object: its Prototype.

text
+-------------------------------------------------------------------------+
|                  THE THREE PROTOTYPE CONCEPTS DEMYSTIFIED               |
+---------------------+-------------------+-------------------------------+
| TERM                | WHAT IT IS        | WHERE IT RESIDES              |
+---------------------+-------------------+-------------------------------+
| [[Prototype]]       | The internal link | Hidden internal slot on EVERY |
|                     | to fallback obj   | JavaScript object instance    |
+---------------------+-------------------+-------------------------------+
| __proto__           | Accessor getter/  | Exists on Object.prototype    |
| (Legacy)            | setter for link   | (Deprecated; use get/setProto)|
+---------------------+-------------------+-------------------------------+
| Constructor.        | The blueprint     | Exists ONLY on functions      |
| prototype           | object assigned   | intended to be used with new  |
+---------------------+-------------------+-------------------------------+
text
+-------------------------------------------------------------------------+
|                  THE PROTOTYPE DELEGATION CHAIN                         |
+-------------------------------------------------------------------------+
|                                                                         |
|  const user = new User("Alice");                                        |
|                                                                         |
|  [ user instance ]                                                      |
|    - name: "Alice" (Own Property)                                       |
|    - [[Prototype]] -----------------------------------+                 |
|                                                       |                 |
|                                                       v                 |
|  [ User.prototype ] <=================================+                 |
|    - login: function() { ... }                                          |
|    - constructor: User                                                  |
|    - [[Prototype]] -----------------------------------+                 |
|                                                       |                 |
|                                                       v                 |
|  [ Object.prototype ] <===============================+                 |
|    - hasOwnProperty: function()                                         |
|    - toString: function()                                               |
|    - valueOf: function()                                                |
|    - [[Prototype]] === null (TERMINATION OF CHAIN!)                     |
|                                                                         |
+-------------------------------------------------------------------------+

The Prototype Chain Lookup Algorithm#

When code accesses a property on an object (e.g., user.login()):

  1. The engine checks user's own properties (user.hasOwnProperty('login')).
  2. If found, it returns the property immediately.
  3. If not found, the engine follows the internal [[Prototype]] link to User.prototype.
  4. It checks User.prototype for 'login'. Found! It executes login() with this bound to user.
  5. If it had not been found, the engine would follow User.prototype.[[Prototype]] to Object.prototype.
  6. If still not found at Object.prototype, it follows to null and returns undefined.

The Shadowing Mechanism on Property Writes#

A vital distinction in prototypal delegation is the asymmetry between reads and writes:

  • Reads traverse down the prototype chain until the property is found.
  • Writes do NOT modify the prototype! Instead, a write creates an Own Property on the instance that shadows (overrides) the prototype's property:
javascript
const proto = { role: "viewer" };
const user = Object.create(proto);

console.log(user.role); // "viewer" (Delegated to proto)

// WRITE: Creates own property on 'user', leaving 'proto' untouched!
user.role = "admin";

console.log(user.role);       // "admin" (Own property)
console.log(proto.role);      // "viewer" (Prototype completely intact!)
console.log(user.hasOwnProperty('role')); // true

3. Prototypal Inheritance vs. Classical Class-Based Inheritance#

Classical Inheritance (Classes as Blueprints) vs. Prototypal Delegation#

In classical languages (C++, Java):

  • Classes are static blueprints defined at compile time.
  • Instantiation copies memory structures: creating an instance copies member definitions into a distinct memory block.
  • Inheritance creates a rigid compile-time type hierarchy.

In JavaScript:

  • Objects are living dynamic entities.
  • Instantiation links objects via pointers ([[Prototype]]).
  • Differential Inheritance: An object only needs to define the specific properties where it differs from its prototype. All shared behaviors remain in a single prototype object in memory, saving massive amounts of RAM.

Modern ES6 `class` Syntax: Syntactic Sugar over Prototypes#

Standardized in ECMAScript 2015 (ES6), the class keyword provides a clean, familiar syntax for object-oriented programming. Underneath the syntax, ES6 classes are functions linked via standard prototype chains:

javascript
// Modern ES6 Class Syntax
class Account {
  constructor(accountNumber, balance = 0) {
    this.accountNumber = accountNumber;
    this.balance = balance;
  }

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

  static getBankCode() {
    return "CHASE_US";
  }
}

// Exactly Equivalent ES5 Prototypal Architecture:
function AccountES5(accountNumber, balance) {
  this.accountNumber = accountNumber;
  this.balance = balance || 0;
}
// Instance methods live on Constructor.prototype
AccountES5.prototype.deposit = function(amount) {
  this.balance += amount;
  return this.balance;
};
// Static methods live directly on the Constructor object
AccountES5.getBankCode = function() {
  return "CHASE_US";
};

Inheritance with `extends` and `super()`#

When one class extends another:

  1. Prototype Chain for Instances: SavingsAccount.prototype.[[Prototype]] is linked to Account.prototype.
  2. Prototype Chain for Static Methods: The SavingsAccount constructor function object itself is linked to Account (SavingsAccount.[[Prototype]] = Account), enabling static method inheritance!
javascript
class SavingsAccount extends Account {
  #interestRate; // Truly private class field (ES2022)

  constructor(accountNumber, balance, interestRate = 0.04) {
    // MUST call super() before accessing 'this'
    super(accountNumber, balance);
    this.#interestRate = interestRate;
  }

  accrueInterest() {
    const interest = this.balance * this.#interestRate;
    this.deposit(interest);
    return interest;
  }

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

const mySavings = new SavingsAccount("SAV_1001", 1000);
mySavings.accrueInterest();
console.log(mySavings.balance); // 1040
// console.log(mySavings.#interestRate); // SyntaxError: Private field must be declared in an enclosing class

The `super()` Calling Mandate:

In derived constructors, you must call `super()` before accessing `this`.

Why? Because in derived classes, the instance object is not created by SavingsAccount. Instead, the base class Account allocates the object in C++ memory, and super() returns that instance back down the constructor hierarchy. Accessing this before super() throws ReferenceError.


4. Modern Metaprogramming: Proxy, Reflect & Symbols#

Metaprogramming refers to code that inspects, intercepts, or modifies the fundamental behavior of the programming language itself. JavaScript provides three primary metaprogramming primitives: `Proxy`, `Reflect`, and `Symbol`.

The `Proxy` Object & Traps#

A Proxy wraps a target object, intercepting low-level operations (such as property reads, property writes, function invocations, and prototype traversals) via handler functions called Traps:

text
+-------------------------------------------------------------------------+
|                  THE JAVASCRIPT PROXY INTERCEPTION MODEL                |
+-------------------------------------------------------------------------+
|                                                                         |
|  Consumer writes: proxy.balance = 500;                                  |
|       |                                                                 |
|       v                                                                 |
|  [ Proxy Handler Trap: set(target, prop, value, receiver) ]             |
|       |                                                                 |
|       |-- Validate: Is value a positive number?                         |
|       |       |-- NO  --> Throw TypeError("Invalid balance")            |
|       |       |-- YES                                                   |
|       v                                                                 |
|  [ Forward mutation to target via Reflect.set() ]                       |
|       |                                                                 |
|       v                                                                 |
|  [ Target Object mutated successfully ]                                 |
|                                                                         |
+-------------------------------------------------------------------------+

Common Proxy Traps:#

  • get(target, prop, receiver): Intercepts property reads (obj.x).
  • set(target, prop, value, receiver): Intercepts property writes (obj.x = 10).
  • has(target, prop): Intercepts the in operator ('x' in obj).
  • deleteProperty(target, prop): Intercepts the delete operator (delete obj.x).
  • apply(target, thisArg, argumentsList): Intercepts function invocations.

The `Reflect` API#

For every Proxy trap, there is a corresponding method on Reflect with identical parameters. Reflect provides the clean, functional alternative to legacy operators:

  • Instead of try { Object.defineProperty(...); } catch(e) {}, Reflect.defineProperty() returns a clean Boolean (true on success, false on failure).
  • Instead of delete obj.prop, use Reflect.deleteProperty(obj, 'prop').
  • Always forward trapped operations to Reflect to preserve prototype context (receiver):
javascript
const validator = {
  set(target, prop, value, receiver) {
    if (prop === "age") {
      if (!Number.isInteger(value) || value < 0) {
        throw new TypeError("Age must be a positive integer");
      }
    }
    // Forward cleanly using Reflect to maintain receiver prototype binding
    return Reflect.set(target, prop, value, receiver);
  }
};

const user = new Proxy({}, validator);
user.age = 25; // Success
// user.age = -5; // Throws TypeError: Age must be a positive integer

Well-Known Symbols & Protocol Extension#

Symbols are unique, immutable primitive identifiers. ECMAScript reserves built-in Well-Known Symbols that allow developers to hook directly into runtime protocols:

  1. `Symbol.iterator`: Defines the iteration protocol. Any object with a [Symbol.iterator] method can be iterated via for...of loops and spread syntax (...obj).
  2. `Symbol.toPrimitive`: Controls how an object coerces to a primitive string or number.
  3. `Symbol.hasInstance`: Customizes the behavior of the instanceof operator.

5. Industrial Implementation: Production Reactive State Store (Proxy-Driven Signals)#

The following complete, executable JavaScript system demonstrates:

  1. A production-grade Proxy-Driven Reactive Store modeled after modern UI engines (Vue 3 and SolidJS).
  2. Automatic dependency tracking: when a function runs inside effect(), any accessed reactive properties automatically subscribe that effect to future changes.
  3. Asynchronous microtask batching to prevent redundant executions when multiple properties mutate synchronously.
javascript
'''
Production Reactive State Engine via ES6 Proxy and Reflect.
Demonstrates dependency tracking, microtask batching, and metaprogramming.
'''

// Global pointer to currently executing reactive effect
let activeEffect = null;

// Target -> Key -> Set of Effects
const targetMap = new WeakMap();

// Microtask scheduler queue for batched updates
const jobQueue = new Set();
let isFlushing = false;

function queueJob(job) {
  jobQueue.add(job);
  if (!isFlushing) {
    isFlushing = true;
    // Batch execution in microtask queue
    queueMicrotask(() => {
      try {
        jobQueue.forEach(fn => fn());
      } finally {
        jobQueue.clear();
        isFlushing = false;
      }
    });
  }
}

// Track dependency (Getter Trap)
function track(target, key) {
  if (!activeEffect) return;

  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }

  dep.add(activeEffect);
}

// Trigger dependents (Setter Trap)
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => {
      queueJob(effect);
    });
  }
}

// Create Reactive Proxy
function reactive(target) {
  if (typeof target !== "object" || target === null) {
    return target;
  }

  return new Proxy(target, {
    get(obj, key, receiver) {
      // 1. Register dependency
      track(obj, key);
      const res = Reflect.get(obj, key, receiver);
      // Support deep reactivity for nested objects
      return typeof res === "object" && res !== null ? reactive(res) : res;
    },
    set(obj, key, value, receiver) {
      const oldValue = obj[key];
      const result = Reflect.set(obj, key, value, receiver);
      // 2. Trigger dependent subscribers if value changed
      if (oldValue !== value) {
        trigger(obj, key);
      }
      return result;
    }
  });
}

// Register a reactive computation
function effect(fn) {
  const effectRunner = () => {
    try {
      activeEffect = effectRunner;
      fn();
    } finally {
      activeEffect = null;
    }
  };
  effectRunner(); // Run initially to collect dependencies
}


// =====================================================================
// VERIFICATION HARNESS
// =====================================================================

function main() {
  console.log("=== PROXY-DRIVEN REACTIVE STATE STORE ===");

  // Create reactive application state
  const state = reactive({
    cluster: {
      name: "us-east-prod",
      activeNodes: 12,
      cpuLoad: 45.5
    },
    trafficGbps: 18.2
  });

  let renderCount = 0;

  // Subscribe reactive UI effect
  effect(() => {
    renderCount++;
    console.log(`[Telemetry UI Updated #{renderCount}] Cluster: {state.cluster.name} | Nodes: {state.cluster.activeNodes} | Traffic: {state.trafficGbps} Gbps`);
  });

  // Mutate state synchronously
  console.log("\n--- Triggering Multiple Synchronous State Mutations ---");
  state.cluster.activeNodes = 14;
  state.trafficGbps = 24.5;
  state.trafficGbps = 28.0;

  console.log("Synchronous code continues executing immediately...");

  // Allow microtask queue to flush
  setTimeout(() => {
    console.log("\n--- After Microtask Flush ---");
    console.log("Notice: Despite 3 consecutive mutations, UI effect updated only ONCE thanks to microtask batching!");
  }, 50);
}

main();

6. Real-World Architectural Case Studies#

Case Study 1: Vue 3 Reactivity Engine Migration from `Object.defineProperty` to `Proxy`#

In Vue 2, the framework achieved reactivity by recursively traversing objects and converting properties to getters and setters using Object.defineProperty().

The Architectural Bottleneck in Vue 2:

  1. Startup Overhead: On large data structures (e.g., a dashboard fetching 10,000 table rows), Vue 2 had to iterate over every key recursively at startup, locking the browser UI for hundreds of milliseconds.
  2. Detection Blindspots: Vue 2 could not detect newly added properties (obj.newProp = 10) or array index assignments (arr[0] = 10). Developers had to use cumbersome utility APIs: Vue.set(obj, 'newProp', 10).

The Vue 3 Solution:

Vue 3 completely rebuilt its reactivity core around `Proxy`:

  • Reactivity is lazy: nested objects are wrapped in proxies only when accessed, making initial data loading 10x faster.
  • Proxies intercept property additions, deletions, and array index assignments natively with zero custom APIs.

Case Study 2: V8 Prototype Pollution Vulnerabilities in Server-Side Node.js#

Prototype Pollution is one of the most dangerous security vulnerabilities in the JavaScript ecosystem (affecting historical versions of lodash.merge, qs, and express).

The Attack Vector:

A vulnerable backend merges untrusted JSON payloads into an internal object:

javascript
// VULNERABLE DEEP MERGE
function deepMerge(target, source) {
  for (const key in source) {
    if (typeof source[key] === "object") {
      if (!target[key]) target[key] = {};
      deepMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

// Attacker sends malicious JSON payload:
const maliciousPayload = JSON.parse('{"__proto__": {"isAdmin": true}}');
deepMerge({}, maliciousPayload);

The Catastrophic Consequence:

Because __proto__ was not filtered, deepMerge accessed Object.prototype directly and injected isAdmin = true.

Now, every single object in the entire Node.js application process has obj.isAdmin === true! Normal unauthenticated users gain administrative access, bypassing all authorization checks.

The Architectural Defense:

  1. Filter dangerous keys: block __proto__, constructor, and prototype.
  2. Use dictionary objects with no prototype: Object.create(null).
  3. Use Map instead of raw plain objects for user-supplied dictionaries.

7. Common Architectural Pitfalls & Anti-Patterns#

Pitfall 1: Modifying Prototypes at Runtime via `Object.setPrototypeOf()`#

javascript
// CATASTROPHIC PERFORMANCE ANTI-PATTERN
const obj = { a: 1 };
Object.setPrototypeOf(obj, customProto); // Wrecks V8 JIT Optimization!

Why this destroys performance: In V8, changing an object's prototype at runtime completely invalidates the Hidden Class (Shape) and wipes out all Inline Caches across every function that has ever interacted with obj. V8 is forced to deoptimize machine code back to slow interpreted bytecode.

The Fix: Always set the prototype at object creation time using Object.create(customProto) or modern class inheritance.

Pitfall 2: Iterating Inherited Properties with `for...in`#

javascript
// ANTI-PATTERN: Leaks inherited properties!
const user = Object.create({ company: "DevThesis" });
user.name = "Alice";

for (const key in user) {
  console.log(key); // Prints "name" AND "company"!
}

for...in iterates over all enumerable properties in the entire prototype chain.

The Modern Fix: `Object.hasOwn()`:

javascript
for (const key in user) {
  if (Object.hasOwn(user, key)) { // ES2022 native replacement for hasOwnProperty
    console.log(key); // Prints strictly own properties: "name"
  }
}
// Or use Object.keys():
Object.keys(user).forEach(key => console.log(key));

8. Checkpoint Practice Problems with Step-by-Step Solutions#

Problem 1: Prototype Chain Traversal & Property Shadowing Trace#

Consider the following prototypal hierarchy:

javascript
function Device(id) {
  this.id = id;
}
Device.prototype.status = "OFFLINE";
Device.prototype.connect = function() {
  this.status = "ONLINE";
};

const d1 = new Device("DEV_01");
const d2 = new Device("DEV_02");

console.log("Trace 1:", d1.status, d2.status);
console.log("Trace 2:", d1.hasOwnProperty("status"));

d1.connect();

console.log("Trace 3:", d1.status, d2.status);
console.log("Trace 4:", d1.hasOwnProperty("status"), d2.hasOwnProperty("status"));

delete d1.status;

console.log("Trace 5:", d1.status);

Required Analysis:

  1. Detail what is logged at Trace 1, Trace 2, Trace 3, Trace 4, and Trace 5.
  2. For each trace, explain whether status was resolved as an Own Property or via prototype delegation.

Solution:

  1. Trace 1: console.log("Trace 1:", d1.status, d2.status);

- Neither d1 nor d2 has an own property named status.

- Engine follows [[Prototype]] to Device.prototype.status.

- Output: "Trace 1: OFFLINE OFFLINE".

  1. Trace 2: console.log("Trace 2:", d1.hasOwnProperty("status"));

- d1 does not own status.

- Output: "Trace 2: false".

  1. `d1.connect()` Execution:

- connect() executes with this = d1.

- Statement: this.status = "ONLINE";.

- Under the write rules of prototypal delegation, this creates an Own Property on d1 with value "ONLINE". Device.prototype.status remains "OFFLINE"!

  1. Trace 3: console.log("Trace 3:", d1.status, d2.status);

- d1.status resolves its own property: "ONLINE".

- d2.status has no own property; delegates to Device.prototype: "OFFLINE".

- Output: "Trace 3: ONLINE OFFLINE".

  1. Trace 4: console.log("Trace 4:", d1.hasOwnProperty("status"), d2.hasOwnProperty("status"));

- d1 owns its shadowed status. d2 does not.

- Output: "Trace 4: true false".

  1. `delete d1.status` & Trace 5:

- delete d1.status deletes the own property from d1.

- Next read of d1.status falls back down the prototype chain to Device.prototype.status.

- Output: "Trace 5: OFFLINE".


Problem 2: ES5 Prototypal Hierarchy vs. ES6 Class Transpilation#

Transpile the following ES6 class hierarchy into clean, production-grade ES5 prototypal code that reproduces identical behavior:

javascript
class Vehicle {
  constructor(vin) {
    this.vin = vin;
  }
  start() {
    return `{this.vin} started`;
  }
}

class ElectricCar extends Vehicle {
  constructor(vin, batteryKwh) {
    super(vin);
    this.batteryKwh = batteryKwh;
  }
  charge() {
    return `{this.vin} charging to {this.batteryKwh}kWh`;
  }
}

Solution:

javascript
// Base Constructor
function Vehicle(vin) {
  this.vin = vin;
}

Vehicle.prototype.start = function() {
  return this.vin + " started";
};

// Derived Constructor
function ElectricCar(vin, batteryKwh) {
  // 1. Call base constructor with 'this' context (Equivalent to super(vin))
  Vehicle.call(this, vin);
  this.batteryKwh = batteryKwh;
}

// 2. Link Prototype Chain using Object.create
// Creates new object with [[Prototype]] pointing to Vehicle.prototype
ElectricCar.prototype = Object.create(Vehicle.prototype);

// 3. Restore constructor pointer (Object.create wipes out constructor reference)
Object.defineProperty(ElectricCar.prototype, "constructor", {
  value: ElectricCar,
  enumerable: false,
  writable: true,
  configurable: true
});

// 4. Add derived methods to prototype
ElectricCar.prototype.charge = function() {
  return this.vin + " charging to " + this.batteryKwh + "kWh";
};

Problem 3: Custom Iterable Data Structure via `Symbol.iterator`#

Design and implement a custom data structure BinaryTree that supports native for...of iteration and array spreading [...tree] in sorted In-Order traversal order using Symbol.iterator and generator delegation.

Solution:

javascript
class TreeNode {
  constructor(value, left = null, right = null) {
    this.value = value;
    this.left = left;
    this.right = right;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(value) {
    const newNode = new TreeNode(value);
    if (!this.root) {
      this.root = newNode;
      return;
    }

    let current = this.root;
    while (true) {
      if (value < current.value) {
        if (!current.left) { current.left = newNode; break; }
        current = current.left;
      } else {
        if (!current.right) { current.right = newNode; break; }
        current = current.right;
      }
    }
  }

  // Implementation of native Iteration Protocol via generator
  *[Symbol.iterator]() {
    yield* this._inOrderTraverse(this.root);
  }

  *_inOrderTraverse(node) {
    if (node !== null) {
      yield* this._inOrderTraverse(node.left);
      yield node.value;
      yield* this._inOrderTraverse(node.right);
    }
  }
}

// Verification Harness
const bst = new BinarySearchTree();
bst.insert(50);
bst.insert(30);
bst.insert(70);
bst.insert(20);
bst.insert(40);

// Native for...of iteration!
console.log("In-Order Iteration:");
for (const val of bst) {
  console.log(val); // 20, 30, 40, 50, 70 (Perfect sorted order!)
}

// Native Array spreading!
const sortedArray = [...bst];
console.log("Sorted Array via Spread:", sortedArray); // [20, 30, 40, 50, 70]

Key Architectural Takeaways

  • JavaScript utilizes prototypal delegation where objects inherit directly from other objects via internal [[Prototype]] slots.
  • Property lookup traverses the prototype chain sequentially until reaching Object.prototype whose prototype is null.
  • A constructor function's .prototype property becomes the [[Prototype]] delegate of all instances created via 'new'.
  • ES6 classes are syntactic sugar over prototypal inheritance, configuring Object.setPrototypeOf under the hood.

Hands-on Code Playground Available

Execute, edit, and experiment with code for this topic in the live sandbox.

Practice Check • Quick Quiz

Test Your Understanding

What is the terminal end of the prototype chain for ordinary JavaScript objects?

A
Function.prototype
B
Object.prototype, whose [[Prototype]] is null
C
window.prototype
D
undefined