JavaScript Fundamentals
Types, scope, coercion, `this` — the base layer everything else in this track sits on.
60 minDifficulty 1/5language · fundamentalsReviewedRead and accepted by a person.
Why this exists
Most JavaScript confusion traces back to four things: how values are copied, how scope works, what `this` refers to, and when the language converts types behind your back. Nail those and the weird behaviours stop being weird — they become consequences of rules you know.
The mental model
Two kinds of box. A primitive box holds the value itself, so copying it duplicates the contents. An object box holds an *address*, so copying it duplicates the address — and both boxes now point at the same thing.
How it works
Primitives copy; objects share
Strings, numbers, booleans, null, undefined, symbols and bigints are copied by value. Objects, arrays and functions are copied by reference. So `const b = a` for an object gives you two names for one thing, and mutating through either is visible through both. This single rule explains most "why did my state change?" bugs, and it's why `const` on an object doesn't prevent mutation — `const` protects the binding, not the contents.
Scope is lexical, and let/const are block-scoped
A function can see the variables where it was *written*, not where it's called from. That's lexical scoping, and it's the foundation closures are built on. `var` is function-scoped and hoisted as `undefined`; `let` and `const` are block-scoped and sit in a temporal dead zone until their declaration line, so touching them early throws instead of silently giving `undefined`. Use `const` by default, `let` when you must reassign, and `var` essentially never.
`this` is set by the call, except in arrow functions
For a normal function, `this` depends on *how it was called*: as a method, it's the object before the dot; called bare, it's `undefined` in strict mode; with `call`, `apply` or `bind`, it's whatever you passed. This is why extracting a method into a variable breaks it — the call site changed. Arrow functions don't have their own `this` at all; they inherit it from the enclosing scope, which is exactly why they're the right choice for callbacks and the wrong choice for object methods.
Coercion is predictable once you use `===`
`==` converts types before comparing, producing genuinely surprising results — `'' == 0` is true, `null == undefined` is true, `'0' == false` is true. `===` compares without converting. Use `===` always; the single exception worth knowing is `x == null`, which neatly catches both `null` and `undefined`. Separately, know the falsy values by heart: `false`, `0`, `-0`, `0n`, `''`, `null`, `undefined`, `NaN`. Everything else is truthy — including `[]` and `{}`, which trips people up constantly.
The mechanism
The distinction that catches everyone: ```js const a = { count: 1 }; const b = a; b.count = 2; // mutation — through the shared reference console.log(a.count); // 2 let c = { count: 1 }; let d = c; d = { count: 99 }; // reassignment — d now points elsewhere console.log(c.count); // 1 ``` **Mutating** reaches through the reference and is visible everywhere. **Reassigning** just repoints one variable. Same-looking code, opposite outcomes — and this is precisely why React state must be replaced rather than mutated.
flowchart TD
A[const b = a] --> B{Is a a primitive?}
B -->|yes| C[b gets a copy of the value]
B -->|no| D[b gets a copy of the reference]
C --> E[Changing b cannot affect a]
D --> F[Mutating through b IS visible via a]
D --> G[Reassigning b does NOT affect a]What people get wrong
- const makes a value immutable.
- It prevents reassigning the binding. The object's contents can still be changed freely. const protects the variable, not the value. Object.freeze gives shallow immutability of the contents.
- Arrow functions are just shorter function syntax.
- They also have no own `this`, no `arguments`, and cannot be used as constructors. The `this` behaviour is the substantive difference. It makes them right for callbacks and wrong for object methods.
- `typeof null` returning 'object' means null is an object.
- It's a bug from the first version of JavaScript, preserved for backwards compatibility. null is its own primitive type. Use `x === null` to test for it; typeof will never help.
- Spread creates a deep copy.
- Spread and Object.assign are shallow — nested objects are still shared by reference. Only the top level is copied. For a deep copy use structuredClone, or copy each nested level explicitly.
When not to use it
- You need guaranteed immutability across a large codebase.
- A library like Immer, or a discipline enforced by review. The language offers only shallow Object.freeze.
- You want compile-time guarantees about shapes and types.
- TypeScript. JavaScript's checks are all at runtime, and by then it's already in production.
Terms
- Primitive
- — A value copied by value: string, number, boolean, null, undefined, symbol, bigint.
- Reference
- — A pointer to an object. Copying it shares the target rather than duplicating it.
- Lexical scope
- — Scope determined by where code is written, not where it's called from.
- Temporal dead zone
- — The span from the start of a block to a let/const declaration, where touching the variable throws.
- Coercion
- — Automatic type conversion, mainly triggered by == and arithmetic operators.
- Falsy
- — The eight values that convert to false: false, 0, -0, 0n, '', null, undefined, NaN.
- Shallow copy
- — Duplicates only the top level. Nested objects remain shared.
In an interview
What's the difference between == and ===, and which should you use?
- == coerces types before comparing; === does not
- concrete surprises like '' == 0 and '0' == false being true
- use === by default
- x == null is the one idiomatic exception, catching null and undefined together
Why does `const arr = []; arr.push(1)` work?
- const prevents reassigning the binding, not mutating the value
- push mutates the existing array rather than creating a new one
- arr = [] would throw, arr.push(1) does not
- Object.freeze is the shallow tool for actually preventing mutation
Can you recall it?
Explain from memory why `const user = { name: 'Ada' }; user.name = 'Grace';` works, but `user = {}` throws.