Deep dive into JS internals, event loop mechanics, closures, and modern TS type safety.
Primitives copy by value and objects copy by reference, so `b = a` duplicates a number but shares an object. That single distinction explains most surprise mutations in a codebase.
`const` prevents reassignment, not mutation: a `const` object's properties can still be changed. `let` is scoped to its block, while `var` is function-scoped and leaks out of loops.
Only backticks interpolate: `${user}` reads the variable, while the identical text in quotes prints literally. Strings are immutable, so every method returns a new one rather than editing in place.
Every JavaScript number is a 64-bit float, which is why `0.1 + 0.2` comes out as `0.30000000000000004`. `%` returns a remainder that keeps the sign of the left operand, so `-7 % 3` is `-1`.
`==` converts types before comparing, which is how `0 == ""` ends up true. `===` compares type and value together, and that is why it is the default and `==` the deliberate exception.
`if` branches on truthiness rather than equality, so `0`, `""`, `null` and `undefined` all take the else path. A ternary evaluates to a value, where `if` only performs a statement.
`undefined` means the name exists holding nothing; not defined means it was never declared and reading it throws. `typeof x` is the only check that survives the second case without crashing.
A function with no `return` gives back `undefined`, not nothing. Arguments are copied in, so reassigning a parameter changes only the local copy unless it is an object, where the reference is shared.
The engine reserves memory for every declaration before it runs a single line, then executes. That two-phase order is why a `function` is callable above its definition while a `var` reads `undefined` there.
Hoisting moves no code anywhere. Memory is reserved during the first phase, so a `function` declaration is fully available early while a `var` merely exists holding `undefined` until its assignment runs.
An arrow function has no `this` of its own and borrows the surrounding scope's. That is exactly what makes it right for callbacks and wrong for object methods, where `this` should be the object.
A variable resolves by where the function was written, never where it was called. The lookup walks outward through each enclosing scope and throws `ReferenceError` only on reaching global empty-handed.
`let` and `const` are hoisted like `var`, but stay unreachable until their declaration runs. Touching one in that gap throws a `ReferenceError` instead of quietly handing back `undefined`.
A returned function keeps the variables it was created beside, even after the outer call has finished and its frame is gone. That is what makes a private `count` possible with no object at all.
Arrays index from 0, so the last element sits at `length - 1` and `arr[arr.length]` is always `undefined`. Nearly every off-by-one bug lives at that one boundary.
Object keys are always strings, so `obj[1]` and `obj["1"]` reach the same slot. `?.` stops a lookup at the first `null` or `undefined` rather than throwing on the property after it.
`.map()` returns a new array of exactly the same length, one output per input. That length guarantee is the whole difference from `.filter()`, and why a map that drops items is the wrong tool.
`.filter()` keeps every element whose test returns truthy and hands back a new array, leaving the original untouched. A test that forgets to `return` filters everything out, because `undefined` is falsy.
`.reduce()` carries an accumulator across the array, and the initial value decides the shape of the result: `0` gives a number, `{}` an object, `[]` an array. Omit it and the first element becomes the seed.
The same `...` gathers on the left and expands on the right: `[a, ...rest]` collects the remainder, `[...a, ...b]` combines two arrays. Both produce new values, so the originals are never mutated.
`{...obj}` copies only the top level, so nested objects stay shared and editing one edits both. `structuredClone()` walks the entire graph, which is why it is the one that actually detaches.
`Object.groupBy()` buckets an array by whatever key the callback returns, replacing the usual `reduce` boilerplate. The result has a null prototype, so it carries no inherited methods to collide with keys.
Passing `fn` hands over the function; `fn()` hands over its result. That one pair of parentheses is the difference between scheduling work for later and having already run it.
`this` is decided by how a function is called, not where it was defined: `user.greet()` binds `user`, while calling it alone binds `undefined`. Arrow functions inherit the enclosing scope.
The microtask queue drains completely before a single task runs, so a `.then()` always beats a `setTimeout(fn, 0)` queued before it. A microtask that queues more microtasks starves timers entirely.
A promise settles once and keeps its result, so a `.then()` attached afterwards still fires. `.catch()` handles a rejection from anywhere earlier in the chain, which is why one at the end usually suffices.
`await` pauses the function, not the thread; everything outside it keeps running. Two sequential awaits cost the sum of both waits, where `Promise.all` costs only the longer of the two.
`catch` only sees errors thrown synchronously inside the `try`. A rejected promise or a callback that throws later escapes it entirely, which is why `await` inside the block is what makes async failures catchable.
A module's code runs once no matter how many files `import` it, and every importer shares that one instance. Named exports must match by name; a default export can be renamed freely at the import site.
A Proxy sits between the code and the object, so `get` and `set` traps run on every property access. That is how observable state and validation work without modifying the original object at all.