JavaScript · Lesson 36

JavaScript Meta Programming: Meta Programming Concepts · Reflect API · Proxy API · Complete Reference

50 phases  ·  Build: Lesson 36 Project

👋 Welcome to Lesson 36

Work through each phase in order. Complete the task before unlocking the next. Your Build It project unlocks when all phases are done.

📚 50 phases🏗️ Lesson 36 Project🌐 GitHub Pages
Phase 1 of 50
The Core Idea

Meta programming means writing code that has knowledge of · or control over · other code at runtime. The prefix "meta" means "about itself": meta programming is programming about programming.

In practical terms, meta programming lets you:

GoalExample
Intercept property accessLog every time any property on an object is read
Intercept property writesValidate data before it's stored on an object
Intercept function callsMeasure how long any function takes to run
Reflect on code structureAsk: "Does this object have this property?"
Create new behaviour dynamicallyBuild objects that respond intelligently to any property name
Protect objectsMake objects that refuse invalid operations

✏️ Your Task
Practise what you just learned about The Core Idea. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 2 of 50
1.1 · Real-World Analogy: The Hotel Concierge

Imagine a hotel with a concierge standing between you and all hotel services. Instead of going directly to housekeeping, the restaurant, or the spa, every request goes through the concierge first.

The concierge can:

  • Allow the request and pass it through unchanged
  • Modify the request before passing it on ("I'll upgrade your room")
  • Reject the request ("The spa is closed")
  • Log the request ("Noting that Room 204 asked for towels at 2pm")
  • Fake a response entirely ("The restaurant is fully booked · I'll tell you it's open to avoid disappointing you")

This is exactly what a Proxy does in JavaScript: it stands between your code and an object, intercepting every interaction.


✏️ Your Task
Practise what you just learned about 1.1 · Real-World Analogy: The Hotel Concierge. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 3 of 50
1.2 · Before Meta Programming: The Limitations

Without meta programming tools, JavaScript has no built-in way to intercept property access. You can't say "run this code whenever anyone reads or writes any property on this object."

You can fake it:

js
// ❌ Manual approach — tedious and doesn't scale:
const user = {
  _name: "Alice",
  get name() {
    console.log("name was read");    // Only works for known properties
    return this._name;
  },
  set name(val) {
    console.log("name was written"); // Must write this for EVERY property
    this._name = val;
  }
};

This only works if you know every property name in advance and manually write getters/setters for each. If the object has 50 properties · or unknown properties · this falls apart.

Meta programming solves this. With Proxy, one handler intercepts operations on any property, known or unknown.


✏️ Your Task
Practise what you just learned about 1.2 · Before Meta Programming: The Limitations. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 4 of 50
1.3 · The Two Pillars: Reflect and Proxy

JavaScript's meta programming is built on two APIs introduced in ES6 (2015):

APIRoleAnalogy
ReflectA clean, functional interface to JavaScript's fundamental operationsA standardised set of tools for directly performing built-in operations
ProxyA wrapper that intercepts operations on an objectThe hotel concierge · intercepts every request

They are designed to work together: Proxy traps intercept operations; Reflect methods perform the default version of those same operations. You'll see this combination constantly.


✏️ Your Task
Practise what you just learned about 1.3 · The Two Pillars: Reflect and Proxy. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 5 of 50
1.4 · What Can Be Intercepted?

JavaScript defines a set of fundamental operations · the basic things that can happen to an object. Both Reflect and Proxy are organised around this same list:

Fundamental OperationWhen It Happens
getReading a property: obj.name
setWriting a property: obj.name = "Alice"
hasChecking existence: "name" in obj
deletePropertyDeleting: delete obj.name
applyCalling a function: fn()
constructCreating with new: new MyClass()
definePropertyObject.defineProperty(obj, ...)
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor(obj, ...)
getPrototypeOfObject.getPrototypeOf(obj)
setPrototypeOfObject.setPrototypeOf(obj, proto)
isExtensibleObject.isExtensible(obj)
preventExtensionsObject.preventExtensions(obj)
ownKeysObject.keys(obj), for...in, Object.getOwnPropertyNames()

Every one of these can be intercepted with a Proxy trap and performed reflectively with Reflect.



# CHAPTER 2 · THE REFLECT API


✏️ Your Task
Practise what you just learned about 1.4 · What Can Be Intercepted?. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 6 of 50
What Is Reflect?

Reflect is a built-in object · not a constructor (you never write new Reflect()) · that provides static methods for interceptable JavaScript operations. Every method on Reflect corresponds directly to a fundamental operation from the list above.

Why does Reflect exist if these operations already worked without it?

Three reasons:

  1. Consistency · Before Reflect, these operations were scattered across Object, function calls, and operators. Reflect puts them all in one place with a uniform API.
  2. Return values · Old methods either threw errors or returned the object for chaining. Reflect methods return meaningful values: true/false for success/failure.
  3. Proxy companion · Inside Proxy traps, Reflect is the clean, safe way to perform the default operation after your custom logic runs.

✏️ Your Task
Practise what you just learned about What Is Reflect?. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 7 of 50
2.1 · `Reflect.get()` · Reading a Property

Equivalent to obj[key] but as a function call:

js
const person = { name: "Alice", age: 30 };

// Traditional:
console.log(person.name);           // Output: Alice
console.log(person["age"]);         // Output: 30

// Reflect:
console.log(Reflect.get(person, "name"));   // Output: Alice
console.log(Reflect.get(person, "age"));    // Output: 30

With a custom this (the receiver argument):

js
const obj = {
  _value: 42,
  get value() { return this._value; }
};

const other = { _value: 99 };

// Read obj's 'value' getter, but with 'other' as 'this':
console.log(Reflect.get(obj, "value", other));   // Output: 99

The third argument receiver overrides what this is inside getters. This is used by Proxy to ensure getters on the target receive the proxy as this, not the raw target.


✏️ Your Task
Practise what you just learned about 2.1 · Reflect.get() · Reading a Property. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 8 of 50
2.2 · `Reflect.set()` · Writing a Property

Equivalent to obj[key] = value but returns a boolean:

js
const car = { brand: "Toyota", speed: 0 };

const success = Reflect.set(car, "speed", 60);
console.log(success);       // Output: true
console.log(car.speed);     // Output: 60

// On a frozen object — returns false instead of throwing:
const frozen = Object.freeze({ x: 1 });
const result = Reflect.set(frozen, "x", 99);
console.log(result);    // Output: false  ← did not succeed
console.log(frozen.x);  // Output: 1     ← unchanged

💡 Why true/false matters: The old way of setting a property (obj[key] = value) silently fails on frozen/sealed objects (or throws in strict mode). Reflect.set always returns false on failure · no exception needed, no silent ignore. Your code can react to the result.


✏️ Your Task
Practise what you just learned about 2.2 · Reflect.set() · Writing a Property. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 9 of 50
2.3 · `Reflect.has()` · Checking Property Existence

Equivalent to the in operator:

js
const book = { title: "1984", author: "Orwell" };

console.log("title"  in book);               // Output: true
console.log(Reflect.has(book, "title"));      // Output: true
console.log(Reflect.has(book, "publisher"));  // Output: false

// Also checks prototype chain, just like 'in':
console.log(Reflect.has(book, "toString"));   // Output: true  ← from Object.prototype

✏️ Your Task
Practise what you just learned about 2.3 · Reflect.has() · Checking Property Existence. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 10 of 50
2.4 · `Reflect.deleteProperty()` · Deleting a Property

Equivalent to delete obj[key], but returns a boolean:

js
const config = { host: "localhost", port: 3000, debug: true };

console.log(Reflect.deleteProperty(config, "debug"));   // Output: true
console.log(config);   // Output: { host: 'localhost', port: 3000 }

// On a non-configurable property — returns false:
const obj = {};
Object.defineProperty(obj, "id", { value: 1, configurable: false });
console.log(Reflect.deleteProperty(obj, "id"));   // Output: false
console.log(obj.id);                               // Output: 1  ← still there

✏️ Your Task
Practise what you just learned about 2.4 · Reflect.deleteProperty() · Deleting a Property. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 11 of 50
2.5 · `Reflect.apply()` · Calling a Function

Equivalent to Function.prototype.apply() but cleaner:

js
function greet(greeting, punctuation) {
  return greeting + ", " + this.name + punctuation;
}

const person = { name: "Alice" };

// Old way:
const result1 = Function.prototype.apply.call(greet, person, ["Hello", "!"]);

// Reflect way — much cleaner:
const result2 = Reflect.apply(greet, person, ["Hello", "!"]);

console.log(result1);   // Output: Hello, Alice!
console.log(result2);   // Output: Hello, Alice!

Reflect.apply(targetFn, thisArg, argsArray) · identical semantics to fn.apply(thisArg, argsArray) but works even if fn.apply has been overridden.


✏️ Your Task
Practise what you just learned about 2.5 · Reflect.apply() · Calling a Function. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 12 of 50
2.6 · `Reflect.construct()` · Calling a Constructor

Equivalent to new Constructor(...args):

js
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

// Traditional:
const p1 = new Point(3, 4);

// Reflect:
const p2 = Reflect.construct(Point, [3, 4]);

console.log(p1);   // Output: Point { x: 3, y: 4 }
console.log(p2);   // Output: Point { x: 3, y: 4 }

Advanced use · constructing an instance of one class using another's constructor:

js
class Base {
  constructor(value) { this.value = value; }
}

class Derived extends Base {}

// Create an instance whose prototype chain follows 'Derived',
// but initialised using 'Base's constructor:
const obj = Reflect.construct(Base, [42], Derived);

console.log(obj instanceof Derived);  // Output: true
console.log(obj.value);               // Output: 42

This is how transpilers (like Babel) implement super() calls.


✏️ Your Task
Practise what you just learned about 2.6 · Reflect.construct() · Calling a Constructor. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 13 of 50
2.7 · `Reflect.defineProperty()` · Defining a Property Descriptor

Equivalent to Object.defineProperty() but returns a boolean instead of throwing:

js
const obj = {};

const success = Reflect.defineProperty(obj, "id", {
  value:        1001,
  writable:     false,
  enumerable:   true,
  configurable: false
});

console.log(success);   // Output: true
console.log(obj.id);    // Output: 1001

✏️ Your Task
Practise what you just learned about 2.7 · Reflect.defineProperty() · Defining a Property Descriptor. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 14 of 50
2.8 · `Reflect.getOwnPropertyDescriptor()` · Reading a Descriptor

Equivalent to Object.getOwnPropertyDescriptor():

js
const user = { name: "Alice" };

const desc = Reflect.getOwnPropertyDescriptor(user, "name");
console.log(desc);
// Output: { value: 'Alice', writable: true, enumerable: true, configurable: true }

✏️ Your Task
Practise what you just learned about 2.8 · Reflect.getOwnPropertyDescriptor() · Reading a Descriptor. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 15 of 50
2.9 · `Reflect.getPrototypeOf()` and `Reflect.setPrototypeOf()`
js
class Animal {}
class Dog extends Animal {}

const rex = new Dog();

// Get:
console.log(Reflect.getPrototypeOf(rex) === Dog.prototype);      // Output: true
console.log(Reflect.getPrototypeOf(Dog.prototype) === Animal.prototype); // Output: true

// Set:
const base = { type: "generic" };
const child = { name: "child" };

Reflect.setPrototypeOf(child, base);
console.log(child.type);   // Output: generic  ← inherited

✏️ Your Task
Practise what you just learned about 2.9 · Reflect.getPrototypeOf() and Reflect.setPrototypeOf(). Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 16 of 50
2.10 · `Reflect.ownKeys()` · All Own Property Keys

Returns all own keys · including non-enumerable ones and Symbol keys:

js
const sym = Symbol("id");
const obj = {
  name:  "Alice",
  age:   30,
  [sym]: 9999
};

Object.defineProperty(obj, "hidden", { value: "secret", enumerable: false });

console.log(Object.keys(obj));           // Output: ['name', 'age']           ← enumerable only
console.log(Reflect.ownKeys(obj));       // Output: ['name', 'age', 'hidden', Symbol(id)]  ← everything

💡 Reflect.ownKeys = Object.getOwnPropertyNames + Object.getOwnPropertySymbols combined into one call.


✏️ Your Task
Practise what you just learned about 2.10 · Reflect.ownKeys() · All Own Property Keys. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 17 of 50
2.11 · `Reflect.isExtensible()` and `Reflect.preventExtensions()`
js
const obj = { x: 1 };

console.log(Reflect.isExtensible(obj));   // Output: true

Reflect.preventExtensions(obj);

console.log(Reflect.isExtensible(obj));   // Output: false
Reflect.set(obj, "y", 2);                 // Fails silently
console.log(obj.y);                       // Output: undefined

✏️ Your Task
Practise what you just learned about 2.11 · Reflect.isExtensible() and Reflect.preventExtensions(). Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 18 of 50
2.12 · Why Use Reflect Instead of Direct Operations?
ScenarioDirect OperationProblemReflect Solution
Delete a propertydelete obj.key returns boolean but looks like a statementConfusing mixed syntaxReflect.deleteProperty(obj, key) → clear boolean
Set on frozen objectobj.key = val silently fails or throwsUnpredictableReflect.set(obj, key, val) → always returns false
Apply with overridden .applyfn.apply(ctx, args) · fails if apply is overriddenFragileReflect.apply(fn, ctx, args) · always works
Inside Proxy trapsNo default behaviour pathMust re-implement manuallyReflect.method(target, ...) · forwards to default

🤔 Thinking question: Reflect.set returns false when setting a property on a frozen object. Object.assign doesn't check the return value of each set · it will silently fail on frozen objects. What problem could this cause in real code?



# CHAPTER 3 · THE PROXY API


✏️ Your Task
Practise what you just learned about 2.12 · Why Use Reflect Instead of Direct Operations?. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 19 of 50
What Is a Proxy?

A Proxy wraps an object (called the target) and intercepts operations performed on it. Every time code tries to read, write, delete, or call anything on the proxy, a trap function runs first.

code
Code ──► Proxy (traps run here) ──► Target Object
                │
          You control what
          happens at each step

✏️ Your Task
Practise what you just learned about What Is a Proxy?. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 20 of 50
3.1 · Creating a Proxy
js
const proxy = new Proxy(target, handler);
ParameterDescription
targetThe original object being wrapped
handlerAn object containing trap functions

Minimal example · a transparent proxy (no traps = pure passthrough):

js
const target = { name: "Alice", age: 30 };
const proxy  = new Proxy(target, {});   // Empty handler = no interception

console.log(proxy.name);   // Output: Alice  ← just like accessing target directly
proxy.age = 31;
console.log(target.age);   // Output: 31  ← changes flow through to target

✏️ Your Task
Practise what you just learned about 3.1 · Creating a Proxy. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 21 of 50
3.2 · The `get` Trap · Intercepting Property Reads
js
const handler = {
  get(target, property, receiver) {
    console.log(`GET: "${property}" was read`);
    return Reflect.get(target, property, receiver);   // Perform the default operation
  }
};

const user = new Proxy({ name: "Alice", role: "admin" }, handler);

console.log(user.name);
// GET: "name" was read
// Output: Alice

console.log(user.role);
// GET: "role" was read
// Output: admin

Trap function signature · every trap follows this pattern:

ParameterMeaning
targetThe original object being proxied
propertyThe property name (or Symbol) being accessed
receiverThe proxy itself (or object inheriting from the proxy)

Reflect.get(target, property, receiver) performs the default get · exactly what would happen without the proxy. Always call the Reflect equivalent at the end of your trap (unless you intentionally want to suppress the default behaviour).


✏️ Your Task
Practise what you just learned about 3.2 · The get Trap · Intercepting Property Reads. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 22 of 50
3.3 · Default Values for Missing Properties

A get trap lets you return custom values for properties that don't exist:

js
const withDefaults = new Proxy({}, {
  get(target, property) {
    if (property in target) {
      return target[property];
    }
    return `[Property "${property}" not found — returning default]`;
  }
});

withDefaults.name = "Alice";
console.log(withDefaults.name);        // Output: Alice
console.log(withDefaults.age);         // Output: [Property "age" not found — returning default]
console.log(withDefaults.anything);    // Output: [Property "anything" not found — returning default]

Real-world use: Configuration objects that return sensible defaults for any unset key, i18n translation maps that return the key name when a translation is missing.


✏️ Your Task
Practise what you just learned about 3.3 · Default Values for Missing Properties. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 23 of 50
3.4 · The `set` Trap · Intercepting Property Writes
js
const handler = {
  set(target, property, value, receiver) {
    console.log(`SET: "${property}" = ${JSON.stringify(value)}`);

    // Validation example:
    if (property === "age" && (typeof value !== "number" || value < 0 || value > 150)) {
      throw new TypeError(`Invalid age: ${value}`);
    }

    return Reflect.set(target, property, value, receiver);   // Perform the actual set
  }
};

const person = new Proxy({}, handler);

person.name = "Alice";
// SET: "name" = "Alice"

person.age = 30;
// SET: "age" = 30

person.age = -5;
// ❌ TypeError: Invalid age: -5

⚠️ The set trap must return true to indicate success (or false / throw to indicate failure). If you forget the return statement, the trap returns undefined (falsy), which JavaScript treats as a set failure · and in strict mode throws a TypeError. Always return Reflect.set(...) or return true.


✏️ Your Task
Practise what you just learned about 3.4 · The set Trap · Intercepting Property Writes. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 24 of 50
3.5 · The `has` Trap · Intercepting `in` Operator
js
const hiddenRange = new Proxy({ min: 1, max: 100 }, {
  has(target, property) {
    // Pretend any number between min and max "exists" in the object:
    const num = Number(property);
    if (!isNaN(num)) {
      return num >= target.min && num <= target.max;
    }
    return Reflect.has(target, property);
  }
});

console.log(50  in hiddenRange);   // Output: true   (50 is between 1 and 100)
console.log(150 in hiddenRange);   // Output: false
console.log("min" in hiddenRange); // Output: true   (actual property)

✏️ Your Task
Practise what you just learned about 3.5 · The has Trap · Intercepting in Operator. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 25 of 50
3.6 · The `deleteProperty` Trap · Intercepting `delete`
js
const protectedObj = new Proxy(
  { name: "Alice", _secret: "password123" },
  {
    deleteProperty(target, property) {
      if (property.startsWith("_")) {
        throw new Error(`Cannot delete protected property: "${property}"`);
      }
      console.log(`Deleting "${property}"`);
      return Reflect.deleteProperty(target, property);
    }
  }
);

delete protectedObj.name;
// Deleting "name"

delete protectedObj._secret;
// ❌ Error: Cannot delete protected property: "_secret"

✏️ Your Task
Practise what you just learned about 3.6 · The deleteProperty Trap · Intercepting delete. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 26 of 50
3.7 · The `apply` Trap · Intercepting Function Calls

When the proxy wraps a function, the apply trap fires whenever the function is called:

js
function multiply(a, b) {
  return a * b;
}

const timedMultiply = new Proxy(multiply, {
  apply(target, thisArg, argumentsList) {
    const start  = performance.now();
    const result = Reflect.apply(target, thisArg, argumentsList);
    const end    = performance.now();
    console.log(`multiply(${argumentsList}) = ${result} [${(end - start).toFixed(3)}ms]`);
    return result;
  }
});

timedMultiply(6, 7);    // multiply(6,7) = 42 [0.012ms]
timedMultiply(100, 50); // multiply(100,50) = 5000 [0.004ms]

Real-world use: Performance profiling, mocking in tests, access control, argument logging, input sanitisation.


✏️ Your Task
Practise what you just learned about 3.7 · The apply Trap · Intercepting Function Calls. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 27 of 50
3.8 · The `construct` Trap · Intercepting `new`
js
class User {
  constructor(name, email) {
    this.name  = name;
    this.email = email;
  }
}

const TrackedUser = new Proxy(User, {
  construct(target, argumentsList, newTarget) {
    console.log(`New ${target.name} created: ${argumentsList[0]}`);
    const instance = Reflect.construct(target, argumentsList, newTarget);
    instance.createdAt = new Date().toISOString();
    return instance;
  }
});

const u = new TrackedUser("Alice", "alice@example.com");
// New User created: Alice
console.log(u.name);       // Output: Alice
console.log(u.createdAt);  // Output: current ISO timestamp

✏️ Your Task
Practise what you just learned about 3.8 · The construct Trap · Intercepting new. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 28 of 50
3.9 · The `ownKeys` Trap · Intercepting Property Enumeration

Controls what Object.keys(), for...in, and Object.getOwnPropertyNames() see:

js
const obj = { name: "Alice", _private: "secret", id: 1 };

const filtered = new Proxy(obj, {
  ownKeys(target) {
    // Hide properties starting with '_':
    return Reflect.ownKeys(target).filter(key => !String(key).startsWith("_"));
  },
  getOwnPropertyDescriptor(target, prop) {
    // Required: tell the engine all returned keys are "real" properties
    return { value: target[prop], enumerable: true, configurable: true, writable: true };
  }
});

console.log(Object.keys(filtered));   // Output: ['name', 'id']  ← _private is hidden
for (const key in filtered) {
  console.log(key);   // Output: name, id  ← _private not enumerated
}

💡 Why the getOwnPropertyDescriptor trap is also needed: When JavaScript checks ownKeys, it then verifies each key by calling getOwnPropertyDescriptor. If the descriptor doesn't show enumerable: true, configurable: true, the engine may throw an error or skip the key. Always pair ownKeys with getOwnPropertyDescriptor when hiding properties.


✏️ Your Task
Practise what you just learned about 3.9 · The ownKeys Trap · Intercepting Property Enumeration. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 29 of 50
3.10 · The `defineProperty` Trap · Intercepting Property Definition
js
const strict = new Proxy({}, {
  defineProperty(target, property, descriptor) {
    if (descriptor.value !== undefined && typeof descriptor.value === "function") {
      throw new TypeError(`Functions cannot be stored on this object.`);
    }
    console.log(`Defining "${property}"`);
    return Reflect.defineProperty(target, property, descriptor);
  }
});

strict.name = "Alice";     // Defining "name"
strict.age  = 30;          // Defining "age"

strict.greet = function() { return "Hi"; };
// ❌ TypeError: Functions cannot be stored on this object.

✏️ Your Task
Practise what you just learned about 3.10 · The defineProperty Trap · Intercepting Property Definition. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 30 of 50
3.11 · The `getPrototypeOf` and `setPrototypeOf` Traps
js
const obj = {};

const liar = new Proxy(obj, {
  getPrototypeOf(target) {
    return Array.prototype;   // Pretend to be an array
  }
});

console.log(Object.getPrototypeOf(liar) === Array.prototype);  // Output: true
console.log(liar instanceof Array);                             // Output: true  ← it's lying!

⚠️ Use this with great caution. Lying about an object's prototype can cause extremely confusing bugs. This trap is primarily useful for implementing virtual objects and testing environments.


✏️ Your Task
Practise what you just learned about 3.11 · The getPrototypeOf and setPrototypeOf Traps. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 31 of 50
3.12 · Revocable Proxies

A revocable proxy can be disabled after creation. Once revoked, any operation on it throws a TypeError.

js
const { proxy, revoke } = Proxy.revocable(
  { name: "Alice", balance: 1000 },
  {
    get(target, prop) {
      return Reflect.get(target, prop);
    }
  }
);

console.log(proxy.name);     // Output: Alice
console.log(proxy.balance);  // Output: 1000

revoke();   // Disconnect the proxy

console.log(proxy.name);
// ❌ TypeError: Cannot perform 'get' on a proxy that has been revoked

Real-world uses of revocable proxies:

  • Session tokens: revoke access after logout
  • Capability-based security: give a module a proxy to a resource, revoke it when done
  • Temporary access windows: allow an operation for exactly N milliseconds, then revoke

✏️ Your Task
Practise what you just learned about 3.12 · Revocable Proxies. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 32 of 50
3.13 · Nested Proxies and the `receiver` Argument

When a proxied object has getters, the receiver argument ensures this inside the getter refers to the proxy (not the raw target):

js
const target = {
  _count: 0,
  get count() { return this._count; }   // 'this' needs to be the proxy to work correctly
};

const handler = {
  get(target, property, receiver) {
    console.log(`Reading: ${property}`);
    return Reflect.get(target, property, receiver);
    //                                    ↑ Pass receiver to Reflect.get
    //                                      so 'this' inside getters is the proxy
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.count);
// Reading: count       ← getter intercepted
// Reading: _count      ← getter's 'this._count' also intercepted!
// Output: 0

Without passing receiver to Reflect.get, the getter's this._count would access target._count directly, bypassing the proxy's get trap.


✏️ Your Task
Practise what you just learned about 3.13 · Nested Proxies and the receiver Argument. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 33 of 50
3.14 · Complete Handler Trap Reference
TrapTriggered ByParametersMust Return
getproxy.prop, proxy[key](target, prop, receiver)Any value
setproxy.prop = val(target, prop, value, receiver)true/false
haskey in proxy(target, prop)Boolean
deletePropertydelete proxy.prop(target, prop)Boolean
applyproxy()(target, thisArg, args)Any value
constructnew proxy()(target, args, newTarget)Object
definePropertyObject.defineProperty(proxy, ...)(target, prop, descriptor)Boolean
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor(proxy, ...)(target, prop)Descriptor or undefined
getPrototypeOfObject.getPrototypeOf(proxy)(target)Object or null
setPrototypeOfObject.setPrototypeOf(proxy, proto)(target, proto)Boolean
isExtensibleObject.isExtensible(proxy)(target)Boolean
preventExtensionsObject.preventExtensions(proxy)(target)Boolean
ownKeysObject.keys(proxy), for...in(target)Array

✏️ Your Task
Practise what you just learned about 3.14 · Complete Handler Trap Reference. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 34 of 50
3.15 · Proxy Invariants (Rules You Cannot Break)

Some traps have invariants · rules the JavaScript engine enforces, even if your trap tries to break them. Violating an invariant throws a TypeError:

InvariantRule
getCannot return a different value for a non-writable, non-configurable property
setMust return false (not true) for non-writable, non-configurable properties
hasCannot return false for a non-configurable own property
deletePropertyCannot return true for a non-configurable property
getPrototypeOfMust return the actual prototype if target is non-extensible
constructMust return an object, not a primitive

These invariants exist to maintain JavaScript's fundamental correctness guarantees. They prevent proxies from completely undermining the type system.



# CHAPTER 4 · META PROGRAMMING REFERENCE


✏️ Your Task
Practise what you just learned about 3.15 · Proxy Invariants (Rules You Cannot Break). Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 35 of 50
4.1 · Complete Reflect API Reference
MethodEquivalent OperationReturns
Reflect.get(target, key, receiver?)target[key]Property value
Reflect.set(target, key, value, receiver?)target[key] = valueBoolean
Reflect.has(target, key)key in targetBoolean
Reflect.deleteProperty(target, key)delete target[key]Boolean
Reflect.apply(fn, thisArg, args)fn.apply(thisArg, args)Return value of fn
Reflect.construct(Cls, args, newTarget?)new Cls(...args)New instance
Reflect.defineProperty(target, key, desc)Object.defineProperty(...)Boolean
Reflect.getOwnPropertyDescriptor(target, key)Object.getOwnPropertyDescriptor(...)Descriptor or undefined
Reflect.getPrototypeOf(target)Object.getPrototypeOf(...)Object or null
Reflect.setPrototypeOf(target, proto)Object.setPrototypeOf(...)Boolean
Reflect.isExtensible(target)Object.isExtensible(...)Boolean
Reflect.preventExtensions(target)Object.preventExtensions(...)Boolean
Reflect.ownKeys(target)Object.getOwnPropertyNames + SymbolsArray of keys

✏️ Your Task
Practise what you just learned about 4.1 · Complete Reflect API Reference. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 36 of 50
4.2 · Complete Proxy Handler Traps Reference

(See the complete table in Section 3.14)


✏️ Your Task
Practise what you just learned about 4.2 · Complete Proxy Handler Traps Reference. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 37 of 50
4.3 · Proxy vs Object.defineProperty · When to Use Each
NeedObject.definePropertyProxy
Intercept one known property✅ Simple and performant✅ Also works
Intercept all properties, known or unknown❌ Must define per-property✅ One handler covers all
Intercept method calls❌ Cannot intercept ()apply trap
Intercept in operator❌ Cannot intercept inhas trap
Intercept delete❌ Cannot intercept deletedeleteProperty trap
Intercept Object.keys()enumerable: false is close but limitedownKeys trap
Revocable access❌ Cannot un-defineProxy.revocable()
Performance-critical hot path✅ Faster (no overhead)⚠️ Some overhead per access

✏️ Your Task
Practise what you just learned about 4.3 · Proxy vs Object.defineProperty · When to Use Each. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 38 of 50
4.4 · Common Meta Programming Patterns
PatternMechanismChapter
Property access loggingget trap3.2
Input validationset trap3.4
Default values for missing propertiesget trap3.3
Read-only objectsset trap that always returns false3.4
Hidden propertiesownKeys + has traps3.5, 3.9
Function timing / profilingapply trap3.7
Constructor injectionconstruct trap3.8
Revocable accessProxy.revocable()3.12
Reactive data bindingset trap triggers re-renderPhase 3
Negative array indexing (arr[-1])get trapExercise 2

✏️ Your Task
Practise what you just learned about 4.4 · Common Meta Programming Patterns. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 39 of 50
4.5 · Symbols and Meta Programming

JavaScript's built-in Symbol values are another form of meta programming · they let you define how objects behave with built-in language features.

js
class Range {
  constructor(from, to) {
    this.from = from;
    this.to   = to;
  }

  // Symbol.iterator — makes Range work in for...of loops:
  [Symbol.iterator]() {
    let current = this.from;
    const last  = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }

  // Symbol.toPrimitive — controls type coercion:
  [Symbol.toPrimitive](hint) {
    if (hint === "number") return this.to - this.from + 1;  // Length of range
    if (hint === "string") return `Range(${this.from}–${this.to})`;
    return true;
  }

  // Symbol.hasInstance — controls instanceof behaviour:
  static [Symbol.hasInstance](instance) {
    return typeof instance === "number";
  }
}

const r = new Range(1, 5);

for (const n of r) process.stdout.write(n + " ");
// Output: 1 2 3 4 5

console.log(`${r}`);     // Output: Range(1–5)
console.log(+r);         // Output: 5  (number of items)
console.log(3 instanceof Range);  // Output: true  (3 is a number!)

Well-known Symbols:

SymbolControls
Symbol.iteratorfor...of, spread [...obj], destructuring
Symbol.toPrimitiveType coercion (number, string, default)
Symbol.hasInstanceinstanceof operator
Symbol.toStringTagObject.prototype.toString.call(obj) display
Symbol.speciesWhich constructor derived classes use
Symbol.isConcatSpreadableWhether Array.prototype.concat spreads the object
Symbol.asyncIteratorfor await...of loops

✏️ Your Task
Practise what you just learned about 4.5 · Symbols and Meta Programming. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 40 of 50
4.6 · Performance Considerations

Proxies add overhead to every intercepted operation. For performance-sensitive hot paths:

js
// Profile proxy overhead:
const plain  = { x: 1 };
const proxied = new Proxy({ x: 1 }, {
  get: (t, k) => Reflect.get(t, k)
});

console.time("plain");
for (let i = 0; i < 1_000_000; i++) plain.x;
console.timeEnd("plain");    // e.g. ~2ms

console.time("proxied");
for (let i = 0; i < 1_000_000; i++) proxied.x;
console.timeEnd("proxied");  // e.g. ~15ms  ← ~7x slower

Guidelines:

  • Use proxies for developer-facing APIs, validation, and tooling · where correctness matters more than microsecond performance
  • Avoid proxies on hot code paths called millions of times per second (tight loops, rendering)
  • Modern engines (V8, SpiderMonkey) optimise simple proxies aggressively; real-world overhead is often negligible


# PHASE 2 · APPLIED EXERCISES


✏️ Your Task
Practise what you just learned about 4.6 · Performance Considerations. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 41 of 50
Exercise 1 · Reflect Practice: Safe Object Operations
🎯 Your Challenge

Objective: Use Reflect methods to perform safer, more predictable object operations than their traditional counterparts.

Scenario: You're building a configuration manager that needs to read, write, and inspect settings safely · even if those settings have been frozen or have custom descriptors.

Warm-up mini-example:

✏️ Task
Practise what you just learned about Exercise 1 · Reflect Practice: Safe Object Operations. Open your editor, type the examples above by hand, modify them, and observe what changes.
js
// Traditional way — throws on frozen objects in strict mode:
const cfg = Object.freeze({ debug: false });
try {
  cfg.debug = true;   // Throws TypeError in strict mode
} catch (e) {
  console.log(e.message);
}

// Reflect way — returns false, never throws:
const result = Reflect.set(cfg, "debug", true);
console.log(result);   // Output: false  — clean signal without exception

Step-by-step instructions:

  1. Create a ConfigManager class that stores settings in a plain object internally.
  2. Write a safeGet(key, defaultValue) method using Reflect.has and Reflect.get.
  3. Write a safeSet(key, value) method using Reflect.set that returns true/false without throwing.
  4. Write a safeDelete(key) method using Reflect.deleteProperty.
  5. Write a listKeys() method using Reflect.ownKeys that separates string keys from Symbol keys.
  6. Test with both normal and frozen objects.

Self-check questions:

  1. What does Reflect.ownKeys() return that Object.keys() does not?
  2. In what scenario would Reflect.set return false even on a non-frozen object?

Phase 42 of 50
Exercise 2 · Proxy: Negative Array Indexing
🎯 Your Challenge

Objective: Use a get trap to support Python-style negative indexing on JavaScript arrays.

Scenario: In Python, arr[-1] returns the last element, arr[-2] the second-to-last, etc. JavaScript does not support this natively. Use a Proxy to add this behaviour.

Warm-up mini-example:

✏️ Task
Practise what you just learned about Exercise 2 · Proxy: Negative Array Indexing. Open your editor, type the examples above by hand, modify them, and observe what changes.
js
const arr  = [10, 20, 30, 40, 50];
const last = arr[arr.length - 1];
console.log(last);   // Output: 50  ← clunky

Step-by-step instructions:

  1. Write a createNegativeArray(arr) function that returns a Proxy-wrapped array.
  2. In the get trap, detect when the property is a numeric string. Convert it to a number.
  3. If the number is negative, convert to arr.length + index before forwarding.
  4. If the number is non-negative or the property is non-numeric (e.g., "length", "push"), fall through to Reflect.get.
  5. Test:
js
const nums = createNegativeArray([10, 20, 30, 40, 50]);
console.log(nums[-1]);   // Output: 50
console.log(nums[-2]);   // Output: 40
console.log(nums[0]);    // Output: 10
console.log(nums.length); // Output: 5
nums.push(60);
console.log(nums[-1]);   // Output: 60  ← works after mutation

Hint:

js
get(target, property, receiver) {
  const index = Number(property);
  if (Number.isInteger(index) && index < 0) {
    return Reflect.get(target, target.length + index, receiver);
  }
  return Reflect.get(target, property, receiver);
}

Self-check questions:

  1. Why must you check Number.isInteger(index) rather than just !isNaN(index)?
  2. What happens to nums.map(), nums.filter(), and other array methods? Do they still work? Why?

Phase 43 of 50
Exercise 3 · Proxy: Type-Enforced Object (Typed Properties)
🎯 Your Challenge

Objective: Create a proxy-based schema validator that enforces type rules on every property write.

Scenario: A form data object for a user registration flow where every field has a required type.

Warm-up mini-example:

✏️ Task
Practise what you just learned about Exercise 3 · Proxy: Type-Enforced Object (Typed Properties). Open your editor, type the examples above by hand, modify them, and observe what changes.
js
// We want this to throw:
const typedUser = createTyped({ name: "string", age: "number" });
typedUser.name = "Alice";   // ✅
typedUser.age  = "thirty";  // ❌ TypeError: age must be a number

Step-by-step instructions:

  1. Write a createTyped(schema) function that accepts an object mapping property names to type strings (e.g., "string", "number", "boolean").
  2. Return a Proxy whose set trap checks typeof value === schema[property] for every write.
  3. If the type doesn't match, throw a TypeError with a descriptive message.
  4. If the property isn't in the schema, throw a RangeError (no unknown properties allowed).
  5. Add a get trap that returns undefined for undefined properties (instead of throwing).

Expected behaviour:

js
const user = createTyped({
  name:    "string",
  age:     "number",
  premium: "boolean"
});

user.name    = "Alice";   // ✅
user.age     = 30;        // ✅
user.premium = true;      // ✅

user.age     = "30";      // ❌ TypeError: "age" must be of type "number", got "string"
user.unknown = "value";   // ❌ RangeError: "unknown" is not a valid property

Self-check questions:

  1. How would you extend this to support "array" as a type? (typeof [] === "object" · tricky!)
  2. Where would you add a "required" validation check that prevents reading before writing?

Phase 44 of 50
Exercise 4 · Proxy: Observable Object (Reactive System Foundations)
🎯 Your Challenge

Objective: Build a mini reactive system where any change to an object automatically notifies registered watchers · the foundation of how Vue.js and MobX work.

Scenario: A state object for a UI component. When any property changes, the UI should re-render.

Step-by-step instructions:

  1. Write a makeObservable(obj, onChange) function that returns a Proxy.
  2. The set trap calls onChange(property, oldValue, newValue) whenever a property changes.
  3. Only fire the callback if the value actually changed (oldValue !== newValue).
  4. The deleteProperty trap should also notify with newValue = undefined.
  5. Test with a simulated "renderer":
✏️ Task
Practise what you just learned about Exercise 4 · Proxy: Observable Object (Reactive System Foundations). Open your editor, type the examples above by hand, modify them, and observe what changes.
js
const state = makeObservable(
  { count: 0, name: "Alice", active: true },
  (prop, oldVal, newVal) => {
    console.log(`[STATE CHANGE] ${prop}: ${JSON.stringify(oldVal)} → ${JSON.stringify(newVal)}`);
    // In a real UI framework, this would trigger a re-render
  }
);

state.count  = 1;     // [STATE CHANGE] count: 0 → 1
state.count  = 1;     // No change — callback NOT fired (same value)
state.name   = "Bob"; // [STATE CHANGE] name: "Alice" → "Bob"
delete state.active;  // [STATE CHANGE] active: true → undefined

Self-check questions:

  1. Why check oldValue !== newValue before firing the callback? What problem does this prevent?
  2. How would you extend this to support watching specific properties rather than all properties?

Phase 45 of 50
Exercise 5 · Proxy: Logging and Profiling Wrapper
🎯 Your Challenge

Objective: Build a reusable function profiler using the apply trap.

Scenario: A development utility that wraps any function and logs its call count, arguments, return value, and execution time.

Step-by-step instructions:

  1. Write a profile(fn, label) function that returns a Proxy wrapping fn.
  2. Use the apply trap to:

- Increment a call counter - Log: [CALL #N] label(args) → result [Xms] - Return the actual result

  1. Add a static getStats(proxy) mechanism · either via a Symbol property on the proxy or a separate WeakMap.
  2. Test with several functions:
✏️ Task
· · # PHASE 3 · PROJECT SIMULATION ·
js
const slowAdd = profile((a, b) => {
  // Simulate work:
  let sum = 0;
  for (let i = 0; i < 1_000_000; i++) sum += i;
  return a + b;
}, "slowAdd");

slowAdd(3, 4);
// [CALL #1] slowAdd(3, 4) → 7 [12.3ms]
slowAdd(10, 20);
// [CALL #2] slowAdd(10, 20) → 30 [11.8ms]

Self-check questions:

  1. Can the profiled function still be called without the proxy? Does the proxy modify the original?
  2. If you profile a method on an object (obj.method), what challenge arises with this? How does thisArg in the apply trap help?


# PHASE 3 · PROJECT SIMULATION


Phase 46 of 50
Project: Reactive State Management Library

Scenario: You are building a lightweight reactive state management library · a simplified version of what powers Vue.js's reactivity system, MobX, and Solid.js. The library must:

  • Track dependencies automatically (which parts of the UI depend on which state)
  • Notify subscribers when state changes
  • Support computed values that update when their dependencies change
  • Prevent direct mutation of state outside authorised setters
  • Provide a read-only view of the state for safe sharing
  • Log all state transitions in development mode

This project uses every meta programming concept from all four chapters: Reflect methods, all major Proxy traps, revocable proxies for read-only views, and Symbols.


Stage 1 · Core Reactive Store

js
// --- Dependency tracking ---
// Which "effect" (subscriber function) is currently running?
let activeEffect = null;
const dependencyMap = new WeakMap();  // target → Map(key → Set(effects))

function track(target, key) {
  if (!activeEffect) return;   // Nothing is watching right now

  if (!dependencyMap.has(target)) {
    dependencyMap.set(target, new Map());
  }
  const keyMap = dependencyMap.get(target);

  if (!keyMap.has(key)) {
    keyMap.set(key, new Set());
  }
  keyMap.get(key).add(activeEffect);   // Record: this effect depends on target[key]
}

function trigger(target, key, oldValue, newValue) {
  if (!dependencyMap.has(target)) return;
  const keyMap = dependencyMap.get(target);
  if (!keyMap.has(key)) return;

  const effects = keyMap.get(key);
  effects.forEach(effect => {
    if (effect !== activeEffect) {   // Don't trigger the currently-running effect
      effect();
    }
  });
}

// --- Reactive Proxy Handler ---
function createReactiveHandler(options = {}) {
  const { readonly = false, debug = false } = options;

  return {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver);

      // Track this access if an effect is watching:
      if (typeof key === "string" || typeof key === "symbol") {
        track(target, key);
      }

      if (debug) {
        console.log(`[REACTIVE GET] ${String(key)} =`, value);
      }

      // If the value is an object, wrap it in a reactive proxy too (deep reactivity):
      if (value !== null && typeof value === "object" && !isProxy(value)) {
        return createReactive(value, options);
      }

      return value;
    },

    set(target, key, value, receiver) {
      if (readonly) {
        console.warn(`[REACTIVE] Attempted to write "${String(key)}" on read-only state.`);
        return false;
      }

      const oldValue = Reflect.get(target, key, receiver);

      if (Object.is(oldValue, value)) return true;   // No actual change

      const success = Reflect.set(target, key, value, receiver);

      if (success) {
        if (debug) {
          console.log(`[REACTIVE SET] ${String(key)}: ${JSON.stringify(oldValue)} → ${JSON.stringify(value)}`);
        }
        trigger(target, key, oldValue, value);   // Notify dependents
      }

      return success;
    },

    deleteProperty(target, key) {
      if (readonly) {
        console.warn(`[REACTIVE] Attempted to delete "${String(key)}" on read-only state.`);
        return false;
      }

      const hadKey    = Reflect.has(target, key);
      const oldValue  = Reflect.get(target, key);
      const success   = Reflect.deleteProperty(target, key);

      if (success && hadKey) {
        trigger(target, key, oldValue, undefined);
      }

      return success;
    },

    has(target, key) {
      track(target, key);
      return Reflect.has(target, key);
    },

    ownKeys(target) {
      track(target, Symbol.for("__ownKeys__"));   // Track enumeration
      return Reflect.ownKeys(target);
    }
  };
}

// Track which objects are already proxied (avoid double-wrapping):
const proxySet = new WeakSet();

function isProxy(obj) {
  return proxySet.has(obj);
}

function createReactive(raw, options = {}) {
  const proxy = new Proxy(raw, createReactiveHandler(options));
  proxySet.add(proxy);
  return proxy;
}

Stage 2 · Store Class with Computed Values and Effects

js
class Store {
  #raw;
  #state;
  #readonlyState;
  #computedCache;
  #debug;

  constructor(initialState, debug = false) {
    this.#raw           = { ...initialState };
    this.#debug         = debug;
    this.#computedCache = new Map();

    // Main reactive state:
    this.#state = createReactive(this.#raw, { debug });

    // Read-only view for safe sharing:
    const { proxy: roProxy, revoke } = Proxy.revocable(
      this.#state,
      createReactiveHandler({ readonly: true, debug })
    );
    this.#readonlyState = roProxy;
    this._revokeReadonly = revoke;   // Store revoke for cleanup
  }

  // Access reactive state:
  get state()   { return this.#state; }

  // Access read-only view:
  get readonly(){ return this.#readonlyState; }

  // Register a side effect that runs whenever its dependencies change:
  effect(fn) {
    const wrappedEffect = () => {
      const prev   = activeEffect;
      activeEffect = wrappedEffect;   // Mark this as the active tracker
      try {
        fn(this.#state);
      } finally {
        activeEffect = prev;          // Restore previous tracker
      }
    };

    wrappedEffect();   // Run once immediately to collect dependencies
    return wrappedEffect;
  }

  // Computed property — cached, recomputes when dependencies change:
  computed(key, computeFn) {
    let cachedValue;
    let dirty = true;   // Flag: needs recomputation

    const recompute = () => { dirty = true; };

    const getter = () => {
      if (dirty) {
        const prev   = activeEffect;
        activeEffect = recompute;   // Track what this computed depends on
        try {
          cachedValue = computeFn(this.#state);
        } finally {
          activeEffect = prev;
        }
        dirty = false;
      }
      return cachedValue;
    };

    this.#computedCache.set(key, getter);
    return getter;
  }

  // Batch multiple mutations — only trigger effects once at the end:
  batch(mutations) {
    const pending = new Set();

    // Temporarily override trigger to collect effects:
    // (Simplified — a production version would queue all triggers)
    mutations(this.#state);
    // Effects fire after the synchronous mutations block completes
  }

  // Snapshot of current state as plain object:
  snapshot() {
    return JSON.parse(JSON.stringify(this.#raw));
  }

  // Clean up — revoke read-only proxy:
  destroy() {
    this._revokeReadonly();
    if (this.#debug) console.log("[STORE] Destroyed — read-only access revoked.");
  }
}

Stage 3 · Using the Store

js
// --- Create a store for a shopping cart ---
const cart = new Store({
  items: [],
  discount: 0,
  user: { name: "Alice", premium: false }
}, true /* debug mode */);

// --- Register an effect: re-render the cart total whenever items or discount change ---
cart.effect(state => {
  const subtotal = state.items.reduce((sum, item) => sum + item.price * item.qty, 0);
  const total    = subtotal * (1 - state.discount);
  console.log(`[CART] ${state.items.length} items | Subtotal: £${subtotal.toFixed(2)} | Total: £${total.toFixed(2)}`);
});
// Runs immediately:
// [CART] 0 items | Subtotal: £0.00 | Total: £0.00

// --- Register an effect: show welcome message when user changes ---
cart.effect(state => {
  console.log(`[USER] Welcome, ${state.user.name} (${state.user.premium ? "Premium" : "Standard"})`);
});
// Runs immediately:
// [USER] Welcome, Alice (Standard)

// --- Computed: item count ---
const itemCount = cart.computed("itemCount", state =>
  state.items.reduce((sum, item) => sum + item.qty, 0)
);

// --- Mutate state ---
cart.state.items.push({ name: "Laptop", price: 899, qty: 1 });
// Triggers cart effect:
// [CART] 1 items | Subtotal: £899.00 | Total: £899.00

cart.state.discount = 0.1;
// Triggers cart effect:
// [CART] 1 items | Subtotal: £899.00 | Total: £809.10

cart.state.user.name = "Bob";
// Triggers user effect:
// [USER] Welcome, Bob (Standard)

console.log("Item count:", itemCount());  // Output: Item count: 1

// --- Read-only view ---
const safeView = cart.readonly;
console.log(safeView.discount);    // Output: 0.1  ← read works
safeView.discount = 0.5;           // [REACTIVE] Attempted to write "discount" on read-only state.
console.log(safeView.discount);    // Output: 0.1  ← unchanged

// --- Snapshot for serialisation ---
const snap = cart.snapshot();
console.log(snap.discount);   // Output: 0.1  ← plain object, safe to serialise

// --- Cleanup ---
cart.destroy();
// [STORE] Destroyed — read-only access revoked.
safeView.discount;
// ❌ TypeError: Cannot perform 'get' on a proxy that has been revoked

Stage 4 · Advanced: Schema-Validated Store

Combine the Proxy validator from Exercise 3 with the reactive store for full type safety:

js
function createValidatedStore(schema, initialState) {
  // First validate the initial state:
  for (const [key, type] of Object.entries(schema)) {
    if (key in initialState && typeof initialState[key] !== type) {
      throw new TypeError(`Initial state: "${key}" must be of type "${type}"`);
    }
  }

  const store = new Store(initialState);

  // Wrap the state proxy in a validation layer:
  const validatedState = new Proxy(store.state, {
    set(target, key, value, receiver) {
      if (key in schema && typeof value !== schema[key]) {
        throw new TypeError(
          `"${String(key)}" must be of type "${schema[key]}", got "${typeof value}"`
        );
      }
      return Reflect.set(target, key, value, receiver);
    }
  });

  return { state: validatedState, store };
}

// Usage:
const { state, store } = createValidatedStore(
  { username: "string", score: "number", active: "boolean" },
  { username: "Alice",  score: 0,       active: true }
);

state.score    = 100;      // ✅
state.username = "Bob";    // ✅
state.score    = "high";   // ❌ TypeError: "score" must be of type "number", got "string"

Reflection Questions:

  1. The store uses a WeakMap for dependencyMap (mapping targets to their effects). Why WeakMap instead of a regular Map? What memory problem does this prevent?
  2. The ownKeys trap tracks with Symbol.for("__ownKeys__"). Why use a Symbol as the tracking key instead of a string like "__ownKeys__"?
  3. The read-only view uses a revocable Proxy. When would you call store.destroy() in a real application? (Think about component lifecycles in a UI framework.)
  4. The isProxy(obj) function uses a WeakSet to track proxied objects. Why not just add a property like obj.__isProxy = true instead?
  5. The current batch() implementation is a placeholder. In a real reactive system, batching prevents effects from running on every single mutation. How would you implement true batching · collecting all effects during mutations, then running each effect exactly once at the end?


# QUIZ & COMPLETION CHECKLIST


✏️ Your Task
Practise what you just learned about Project: Reactive State Management Library. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 47 of 50
Self-Assessment Quiz

Q1: What is meta programming, and what two ES6 APIs are its primary tools in JavaScript?

Q2: What is the difference between Reflect.set(target, key, value) and target[key] = value?

Q3: Write a Proxy that logs every property read with the property name and value.

Q4: What are "Proxy traps" and "invariants"? Give one example of each.

Q5: What is a revocable Proxy, and when would you use one?

Q6: What does the receiver argument in a get trap do, and why does it matter?

Q7: What is the difference between Reflect.ownKeys(obj) and Object.keys(obj)?

Q8: Why is return Reflect.set(target, key, value, receiver) better than return true at the end of a set trap?

Q9: What does the apply trap intercept, and what does it receive as arguments?

Q10: In what order are these evaluated: direct property access, Proxy get trap, Object.defineProperty getter? Where does Proxy fit in this chain?


✏️ Your Task
Practise what you just learned about Self-Assessment Quiz. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 48 of 50
Answer Key

A1: Meta programming is writing code that reads, controls, or transforms other code at runtime. The two primary tools are Reflect (a functional interface to fundamental JS operations) and Proxy (an interceptor that wraps objects and intercepts operations on them).

A2: target[key] = value silently fails on frozen/non-writable properties (or throws in strict mode) and returns the value, not a success indicator. Reflect.set(...) always returns a boolean (true = success, false = failure) without throwing · consistent across all contexts.

A3:

js
const logged = new Proxy(obj, {
  get(target, property, receiver) {
    const value = Reflect.get(target, property, receiver);
    console.log(`GET "${String(property)}" = ${JSON.stringify(value)}`);
    return value;
  }
});

A4: A trap is a method on the Proxy handler that intercepts a specific operation (e.g., get intercepts property reads). An invariant is a rule the engine enforces even if your trap tries to break it · e.g., a get trap cannot return a different value for a non-writable, non-configurable property.

A5: A revocable Proxy is created with Proxy.revocable() and returns both proxy and revoke. Calling revoke() permanently disables the proxy · all further operations throw a TypeError. Used for: session expiry, temporary capability grants, component cleanup.

A6: receiver is the Proxy itself (or an object that inherits from it). Passing receiver to Reflect.get(target, key, receiver) ensures that when a getter runs on the target, this inside that getter refers to the proxy · not the raw target. Without it, getters bypass proxy traps when accessing this.property.

A7: Object.keys(obj) returns only own, enumerable, string-keyed properties. Reflect.ownKeys(obj) returns all own properties · including non-enumerable, non-configurable, and Symbol-keyed ones.

A8: return true unconditionally claims success. return Reflect.set(target, key, value, receiver) reports the actual outcome · if the underlying set fails (frozen property, failed setter, etc.), it correctly returns false. Lying with return true violates the trap's invariant contract.

A9: The apply trap intercepts function calls: proxy(), proxy.call(ctx, args), proxy.apply(ctx, args). It receives (target, thisArg, argumentsList) · the original function, the value of this, and an array of all arguments.

A10: When you access a property on a proxied object: the Proxy get trap fires first. Inside the trap, Reflect.get(target, key, receiver) is called, which consults the target's actual property · including any Object.defineProperty getter defined on it. Proxy traps intercept at the highest level; descriptor getters run when the Reflect operation executes on the underlying target.


✏️ Your Task
Practise what you just learned about Answer Key. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 49 of 50
Key Gotchas Summary
MistakeWhy It HappensFix
Forgetting return in set trapReturns undefined (falsy) → strict mode throwsAlways return Reflect.set(...) or return true/false
Not passing receiver to Reflect.getGetters on target use raw target as this, bypassing proxyReflect.get(target, key, receiver) · always pass it
Double-wrapping objects in get trapNested object gets wrapped in new proxy on every readTrack already-proxied objects with WeakSet
Using proxy as this in trapInside a trap, this is the handler, not the proxy or targetUse target or receiver explicitly
Breaking invariantsReturning inconsistent values for non-configurable propertiesTest with strict mode; check the invariant table
ownKeys without getOwnPropertyDescriptorEngine verifies each returned key · missing descriptor causes errorPair ownKeys with getOwnPropertyDescriptor trap
Losing revoke referenceNo way to revoke a revocable proxyStore both proxy and revoke from Proxy.revocable()
Proxy on hot pathEvery trap call adds overheadProfile first; avoid proxy on million-calls-per-second paths
Proxying the wrong objectSetting a trap on a copy instead of the originalnew Proxy(originalObject, handler) · proxy wraps the original
Symbol.for() vs Symbol()Symbol() creates unique symbols · Symbol.for(key) returns shared global oneUse Symbol.for(key) only for intentionally shared symbols

✏️ Your Task
Practise what you just learned about Key Gotchas Summary. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 50 of 50
One-Sentence Summary

JavaScript meta programming · built on Reflect's clean functional interface to fundamental operations and Proxy's ability to intercept any interaction with any object · is what powers reactive frameworks, validation libraries, ORM query builders, and developer tools: code that programs the behaviour of other code at runtime.


Tutorial generated by AI_TUTORIAL_GENERATOR · Source curriculum: W3Schools JavaScript Meta Programming (4 pages)

✏️ Your Task
Practise what you just learned about One-Sentence Summary. Open your editor, type the examples above by hand, modify them, and observe what changes.
🏗️ Build It — Mini Project
Lesson 36 Project

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>My Project</title> </head> <body> <!-- Your code h

starter.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Project</title>
</head>
<body>
<!-- Your code here -->
</body>
</html>

Lesson 36 complete! 🎉

You covered: