Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

27 chapters
⌕
Beginner9›
B1Setup & mental modelB2Types & valuesB3Operators & flowB4Functions (first half)B5Objects & arrays (first half)B6DOM & eventsB7Basic asyncB8Errors & tools★Cheat page
Intermediate10›
I1Scope & functions, properlyI2Objects deeplyI3Prototypes & OOPI4Async, properlyI5Modules & toolingI6Regex, dates & APIsI7Error handlingI8Real-time connectionsI9Offline & storage★Cheat page
Advanced10›
A1Engine & memoryA2Advanced asyncA3MetaprogrammingA4Types & dataA5Patterns & architectureA6PerformanceA7SecurityA8EcosystemA9Testing★Cheat page
/ search[ ] chaptert top

JavaScript levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
A3

Metaprogramming

Code that changes how ordinary-looking code behaves.

Symbol — a key that can never collide

Every Symbol() call creates a value that's unique, even against another symbol created with the exact same description — it exists specifically to be usable as a property key that can never accidentally collide with a string key some other piece of code happens to also use.

const id = Symbol("id");
const obj = { name: "Ana", [id]: 42 };
Object.keys(obj);          // ["name"] — symbol keys are invisible to normal enumeration
obj[Symbol("id")];         // undefined — a DIFFERENT symbol, even with the identical description
obj[id];                    // 42 — only the exact same symbol reference works

JS itself uses a handful of well-known symbols as hooks the engine calls automatically at specific moments — this is the actual mechanism behind several "special" behaviors from earlier chapters.

Symbol Called when
Symbol.iteratorfor...of, spread, or destructuring needs to walk the object's values
Symbol.toPrimitivethe object is used where a primitive is needed — +obj, template interpolation, obj + ""
Symbol.hasInstanceinstanceof checks this object as the right-hand side
Symbol.toStringTagObject.prototype.toString.call(obj) builds its "[object X]" label
class Money {
  constructor(amount) { this.amount = amount; }
  [Symbol.toPrimitive](hint) {
    if (hint === "number") return this.amount;
    if (hint === "string") return "$" + this.amount.toFixed(2);
    return "Money(" + this.amount + ")";
  }
}
const price = new Money(9.5);
console.log(+price);        // "number" hint — what happens?
console.log(`${price}`);  // "string" hint — what happens?
console.log(price + "");    // "default" hint — what happens?

9.5, then "$9.50", then "Money(9.5)" — the exact same object gives three different answers, because the engine tells Symbol.toPrimitive which conversion it's trying to do. This is the real mechanism behind why +new Date() gives a timestamp while `${new Date()}` gives a readable string — same object, hint-aware conversion.

Iteration protocols, formally

Two related but separate contracts. An object is iterable if it has a [Symbol.iterator]() method that returns an iterator — and an iterator is just any object with a .next() method that returns { value, done }. That's the entire protocol for...of, spread, and destructuring are all built on — which is exactly why the custom Range class below works with every one of them for free, the moment it implements one method.

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        return current < end
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      },
    };
  }
}
console.log([...new Range(1, 5)]);   // what happens?

[1, 2, 3, 4] — spread never needed to know Range exists as a concept. It only ever asked "does this have Symbol.iterator?", called it, and kept calling .next() until done came back true. Generators are just a shortcut for writing exactly this object without building it by hand — every generator already implements this protocol for you. Async iteration is the same shape with one difference: [Symbol.asyncIterator]() instead, and .next() returns a promise of { value, done }, which is what for await...of knows how to unwrap.

Proxy and Reflect

A Proxy wraps an object and lets you intercept the fundamental operations on it — get, set, has, deleteProperty, and more — with your own function, called a trap. Reflect is the companion: the same set of operations, exposed as plain functions, so a trap can perform the real default behavior after doing its own work, instead of re-implementing it by hand.

const target = { name: "Ana", age: 29 };
const logged = new Proxy(target, {
  get(obj, prop) {
    console.log("GET", String(prop));
    return Reflect.get(obj, prop);   // the real, normal read
  },
  set(obj, prop, value) {
    console.log("SET", String(prop), "=", value);
    return Reflect.set(obj, prop, value);   // the real, normal write
  },
});

logged.name;
logged.age = 30;

Every single property access on logged — reads and writes both — is now observable, without target itself ever knowing it's being watched. This exact shape (intercept, log or validate, then delegate to Reflect) is the whole mechanism behind validation libraries, ORMs that track which fields changed, and framework reactivity.

function createValidated(schema) {
  return new Proxy({}, {
    set(obj, prop, value) {
      if (schema[prop] && typeof value !== schema[prop]) {
        throw new TypeError(String(prop) + " must be a " + schema[prop]);
      }
      return Reflect.set(obj, prop, value);
    },
  });
}
const user = createValidated({ age: "number" });
user.age = "nope";   // throws immediately — invalid data can't even be assigned

Object.defineProperty vs Proxy — Vue 2 vs Vue 3

Before Proxy existed everywhere, reactive frameworks used Object.defineProperty to turn each property into a getter/setter pair that could track reads and notify on writes — this was Vue 2's actual reactivity engine, property by property.

Object.defineProperty (Vue 2) Proxy (Vue 3)
New properties added laterinvisible — never converted, needed a special Vue.set()caught automatically — the trap fires for any key
Arraysindex writes and length changes needed special-cased method overridesjust works — array mutation is property access too
Setup costwalks every property up front, recursivelywraps once — nested objects are wrapped lazily, on first access

Invariants Proxy has to respect

A trap isn't a completely free rewrite of an object's behavior — a handful of invariants are enforced by the engine no matter what a trap tries to return, mostly around Object.freeze. A get trap on a frozen, non-configurable, non-writable property must return the real, actual value — returning anything else throws a TypeError. This exists so Object.freeze's guarantee from two chapters back stays a real guarantee, not something a misbehaving Proxy trap could quietly undermine.

eval and new Function — and why almost never

eval("console.log(1 + 1)");           // runs in the CALLING scope — can read/write local variables
new Function("a", "b", "return a + b");  // runs in GLOBAL scope only — can't see any local variable

Both compile and run a string as code, and both come with the same three costs: the engine can't statically analyze code that doesn't exist yet at parse time, so it gets none of the optimization this whole chapter has been about; a strict Content-Security-Policy (covered next chapter) blocks them outright; and if that string ever contains anything derived from user input, it's arbitrary code execution, full stop — not a bug class, the actual worst case.

⚠ This site's own code runner uses new Function Every .try block on this page, and the whole practice playground, really does run your code through new Function(...) inside a Web Worker — that's not a contradiction of the warning above, it's the actual legitimate use case: a sandboxed worker with no DOM access, running code the reader explicitly chose to execute, not untrusted input silently reaching eval in a real production app.
Practice this layer

Opens in the editor — write it, run it, and check it against real tests.

A Fibonacci class you can spread3 tests · advancedAn array that rejects non-positive numbers3 tests · advanced
←previousAdvanced async↑ CovernextTypes & data→