How to read this post
This is the wrap-up for the JavaScript series. The previous eight posts were tutorials — they build mental models from first principles. This post is a reference: short answers to the deeper “what is this and why does it work that way” questions that the earlier tutorials hint at but rarely linger on.
Use the headings as an index. Each section is self-contained.
Type system
What is “duck typing”?
If it walks like a duck and quacks like a duck, treat it as a duck.
JavaScript has no interface keyword (TypeScript does, but those vanish at compile time). At runtime, code decides whether an object is suitable by checking the shape — does it have the methods I am about to call? — rather than its declared type.
1function play(media) {
2 if (typeof media.play !== "function") throw new TypeError("not playable");
3 return media.play();
4}
This is duck typing: the function works on any value that exposes a play method, regardless of how it was constructed.
Why does JavaScript need a “host environment”?
The ECMAScript language defines syntax, semantics, and the built-in library (Array, Promise, Math, …). It does not define how to read a file, open a network connection, display a button, or paint a pixel. Those capabilities are provided by a host:
- The browser host adds
window,document,fetch,localStorage,setTimeout,IndexedDB, … - Node.js adds
fs,process,Buffer,EventEmitter,http, … - Deno and Bun add their own (mostly browser-compatible) host APIs.
- Embedded engines (V8 in databases, JavaScriptCore in IoT) supply whatever the embedder exposes.
The same source code can run in any of them — as long as it touches only the standard library or host features known to be present.
Variables and properties
What is the difference between a variable and a property?
A variable is a binding in a lexical scope (introduced by let, const, var, function, class, import, or a function parameter).
A property is a key on an object — accessed with .name or ["name"].
The two collide at the top level: in non-module scripts a top-level var becomes a property of the global object (window / globalThis). Top-level let and const do not — they live in a separate “script scope”.
What is a “leaking global”?
An assignment to an undeclared identifier in non-strict mode creates a property on the global object:
1function bad() {
2 total = 10; // no var/let/const → creates window.total
3}
In strict mode (automatic inside ES modules and class bodies), the same assignment throws a ReferenceError — which is the modern way to prevent the leak.
Property descriptors — the four-attribute story
Every property has a descriptor with four slots: value, writable, enumerable, configurable (or get, set, enumerable, configurable for accessors).
1const obj = {};
2Object.defineProperty(obj, "PI", {
3 value: 3.14159,
4 writable: false,
5 enumerable: true,
6 configurable: false,
7});
8obj.PI = 4; // silently ignored (throws in strict mode)
9delete obj.PI; // false in strict mode
The full attribute matrix is covered in Modern object features.
How do you iterate over non-enumerable properties?
1Object.getOwnPropertyNames(obj) // all string-keyed own properties, including non-enumerable
2Object.getOwnPropertySymbols(obj) // all symbol-keyed own properties
3Reflect.ownKeys(obj) // strings AND symbols
Object.keys and for...in only see enumerable string keys, by design.
Memory model
Value semantics vs reference semantics
Primitives — number, string, boolean, bigint, symbol, null, undefined — are values. Assignment copies the value:
1let a = 1; let b = a; b = 2;
2a; // still 1
Objects — including arrays, functions, and class instances — are references. Assignment copies the reference; both variables point at the same object:
1const a = { x: 1 }; const b = a; b.x = 99;
2a.x; // 99
This rule explains every “I changed something I didn’t mean to” bug in JavaScript. See the Objects basics post for cloning strategies.
What is a WeakMap / WeakSet for?
A Map keeps strong references to its keys, preventing them from being garbage-collected. WeakMap keys are weakly held — when no other reference exists, both the key and its associated value become eligible for collection.
Practical uses:
- Attaching private data to objects you do not own (DOM nodes, library instances).
- Caches keyed by object identity that should not extend lifetimes.
1const meta = new WeakMap();
2meta.set(domNode, { renderedAt: Date.now() });
3// when domNode is removed from the DOM and no other reference exists,
4// the entry is cleaned up automatically.
WeakMap keys must be objects — primitives cannot be weakly held.
Symbols and uniqueness
When should I use Symbol?
Three cases that come up often:
- Private-like keys on objects you share with other code:
1const tag = Symbol("tag"); 2obj[tag] = "internal"; // invisible to Object.keys / for...in - Enumerated constants that should never collide with strings:
1const states = { Loading: Symbol("loading"), Ready: Symbol("ready") }; - Protocol participation — the well-known symbols (
Symbol.iterator,Symbol.toPrimitive,Symbol.hasInstance, …) let your objects integrate withfor...of, coercion,instanceof, and more. See the Modern object features post.
Symbol.for(key) consults a global registry — useful when the same symbol must be shared across realms (iframes, workers). Symbol(key) always returns a fresh symbol.
Iteration and generators
Iterable vs iterator vs generator — which is which?
| Term | Definition |
|---|---|
| Iterable | An object with [Symbol.iterator]() that returns an iterator. |
| Iterator | An object with .next() returning { value, done }. |
| Generator | A function declared with function*. When called, it returns an iterator that is also iterable. |
Every generator is both an iterable and an iterator (it has its own [Symbol.iterator] that returns itself). That is why for...of and spread “just work” on generator results.
When to reach for a generator
- Lazy sequences (infinite ranges, parser tokens).
- Tree / graph walks where you want the caller to control iteration speed.
- Coroutine-style patterns before
async/awaitexisted (still useful for stateful protocols).
See Modern function features for working examples.
The event loop
Why is JavaScript single-threaded but non-blocking?
A JavaScript runtime runs the application code on one thread — the main thread — driven by an event loop. Long-running work yields control to the loop, which then dispatches queued tasks one at a time.
There are two queues that matter:
- Macrotask queue —
setTimeout,setInterval, I/O callbacks in Node,MessageChannel, UI events in browsers. - Microtask queue — promise reactions,
queueMicrotask, andMutationObservercallbacks.
After every macrotask, the loop drains all pending microtasks before picking the next macrotask. That ordering explains output like:
1setTimeout(() => console.log("timeout"), 0);
2Promise.resolve().then(() => console.log("promise"));
3console.log("sync");
4
5// sync
6// promise
7// timeout
The synchronous code runs first, the promise reaction is a microtask scheduled before the timeout’s macrotask, so it logs second.
Why does a for loop block the UI?
Browsers paint between macrotasks. A long synchronous loop holds the main thread, so no paint happens until the loop ends — the page appears frozen. The fixes:
- Break work into chunks and yield with
setTimeout(fn, 0),requestIdleCallback, orawait new Promise(r => setTimeout(r, 0)). - Move the work to a Web Worker (browser) or worker_threads (Node).
- Use
requestAnimationFramefor visual updates.
Equality and comparison
How many equality operations does JavaScript have?
Four:
| Operation | Notes |
|---|---|
== | Loose — coerces operand types. Avoid except x == null. |
=== | Strict — same type and same value. The default. |
Object.is(a, b) | Like === except Object.is(NaN, NaN) === true and Object.is(0, -0) === false. |
SameValueZero | The internal algorithm used by Set, Map, and Array.prototype.includes. Same as === except NaN is equal to itself. |
Why is [] == false true?
Loose equality runs a six-step coercion. [] becomes "" (the array’s default string conversion), "" becomes 0, false becomes 0, and 0 == 0 is true. Almost no real code wants this — use ===.
Error handling
throw, try, catch, finally — the working subset
1try {
2 doWork();
3} catch (err) {
4 if (!(err instanceof DomainError)) throw err; // re-throw what you don't know
5 recover(err);
6} finally {
7 cleanUp();
8}
Two modern essentials:
catchwithout a binding —catch { ... }if you do not need the error value.Error.cause— attach an underlying error:throw new Error("upload failed", { cause: networkErr }). Loggers and DevTools show the full chain.
Why prefer custom error classes?
1class ValidationError extends Error {
2 constructor(message, field) {
3 super(message);
4 this.name = "ValidationError";
5 this.field = field;
6 }
7}
Custom classes give you instanceof filtering in catch blocks, structured fields for logging, and consistent names in stack traces.
Modules and packaging
(Detailed in JavaScript Modules. Two questions worth repeating here.)
Why does Node treat .js as CommonJS by default?
Historical: CommonJS predates ESM by a decade, and Node’s huge ecosystem was built on require(). The "type": "module" field in package.json is the opt-in to make .js mean ESM for new code.
Why are ES module imports static?
Static import/export allow:
- Tree-shaking — bundlers know exactly which exports are reachable.
- Cycle detection at load time, with predictable resolution.
- Asynchronous fetch in browsers — the import graph can be assembled and fetched in parallel before any code runs.
Dynamic import() exists for the cases where static is too rigid.
Performance and tooling tips
- Measure, don’t guess. Use
console.time, the Performance panel, ornode --prof. - Avoid
Object.assign({}, big, ...)in hot paths. Spread is faster in modern engines. - Prefer
fororfor...ofoverforEachfor tight loops. Function-call overhead matters. MapandSetoutperformObjectfor frequent insert/delete or membership tests.structuredCloneis fast and correct for plain data. Skip JSON round-tripping.- Profile bundle size with
source-map-explorerorrollup-plugin-visualizer. The biggest performance win is usually shipping less code.
Where to go next
- The TC39 process at tc39.es — every new feature, in stages 0–4.
- The MDN Web Docs — the canonical reference for every API mentioned here.
- The V8 blog — deep dives on optimisation, memory, and the engine’s roadmap.
This concludes the JavaScript series. If you spotted gaps you would like covered, let me know — there is always room for another post.










