
JavaScript interviews love questions that look trivial but test deep understanding: why does [] == false return true, why does a var loop print the same number three times, and what exactly is a closure? All of them come from a handful of fundamentals: types, equality, scope, hoisting and closures.
This is Part 5 of the Learn Frontend series. We cover these core concepts, plus the array methods and modern syntax you will use every day in React code.
1. Types: primitives vs references
JavaScript has seven primitive types: string, number, bigint, boolean, undefined, null and symbol. Everything else (objects, arrays, functions, dates) is an object.
- Primitives are copied by value and are immutable.
- Objects are handled by reference: assigning an object to another variable copies the reference, so both variables point to the same object. Mutating through one is visible through the other. This is exactly why React state must be updated immutably (Part 11).
typeof nullis"object", a historical bug. UseArray.isArray()to detect arrays.- Copying:
{...obj}and[...arr]make shallow copies;structuredClone(obj)makes a deep copy.
2. Equality and type coercion
=== (strict equality) compares type and value with no conversion. == (loose equality) converts types first, producing surprises such as 0 == '', null == undefined and [] == false all being true.
- Use
===almost always. The one common exception isx == null, which checks for both null and undefined. - Falsy values:
false,0,-0,0n,'',null,undefined,NaN. Everything else is truthy, including[],{}and"0". NaN !== NaN; useNumber.isNaN().??(nullish coalescing) only falls back for null or undefined, unlike||, which also replaces 0 and empty strings.
3. Scope, var, let and const, and hoisting
var | let | const | |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted | Yes, initialised to undefined | Yes, but in the temporal dead zone | Yes, but in the temporal dead zone |
| Re-assignable | Yes | Yes | No (but object contents can change) |
Hoisting means declarations are processed before code runs. var variables exist from the start of the function with the value undefined; let and const exist but cannot be touched until their declaration line (the temporal dead zone), so accessing them early throws a ReferenceError. Function declarations are hoisted completely, so you can call them before they appear; function expressions and arrow functions are not.
console.log(a); // undefined (var is hoisted and initialised)
var a = 1;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2; // b was in the temporal dead zone
greet(); // works: function declarations are fully hoisted
function greet() { console.log('hi'); }JavaScript uses lexical scope: a function can see variables from where it was written, not from where it is called. Inner scopes can read outer variables, forming a scope chain.
4. Closures
A closure is a function together with the variables from the scope where it was created. The inner function keeps access to those variables even after the outer function has returned. Closures power private state, factories, memoization, event handlers and React hooks.
function makeCounter() {
let count = 0; // private: only the closure can reach it
return {
increment: () => ++count,
get: () => count,
};
}
const c = makeCounter();
c.increment(); c.increment();
console.log(c.get()); // 2
// Classic interview question
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3 3 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j)); // 0 1 2The classic loop bug happens because var creates one shared variable for the whole loop, so every callback sees its final value. let creates a fresh binding for each iteration, so each callback closes over its own value.
5. Functions and array methods
Functions are values: you can pass them as arguments and return them from other functions (higher-order functions). The array methods built on this are used constantly in frontend code.
const orders = [
{ id: 1, amount: 499, status: 'paid' },
{ id: 2, amount: 1299, status: 'pending' },
{ id: 3, amount: 899, status: 'paid' },
];
const paidTotal = orders
.filter(o => o.status === 'paid') // keep paid orders
.map(o => o.amount) // take amounts
.reduce((sum, x) => sum + x, 0); // add them up
console.log(paidTotal); // 1398
const byStatus = Object.groupBy(orders, o => o.status); // { paid: [...], pending: [...] }map,filterandreducereturn new values and do not mutate the original array, which suits React.forEachreturns nothing; use it only for side effects.sort,reverseandsplicemutate in place;toSorted,toReversedandtoSplicedare the non-mutating versions.find,some,everyandincludesanswer common questions without loops.
6. Modern syntax you will use daily
- Destructuring:
const { name, age = 18 } = user;andconst [first, ...rest] = list; - Spread and rest:
{...user, age: 30}copies and overrides;function sum(...nums)collects arguments. - Optional chaining:
user?.address?.cityreturns undefined instead of throwing. - Template literals:
`Hello ${name}`. - Arrow functions: shorter syntax and no own
this(Part 6 explains why that matters). - Modules:
importandexportsplit code into files; bundlers combine them (Part 15). - Map and Set: keyed collections with any key type, and unique values.
- Primitives are copied by value; objects are shared by reference, so update them immutably.
- Prefer === ; know the falsy values; use ?? when 0 or empty string are valid.
- var is function-scoped and hoisted as undefined; let and const are block-scoped with a temporal dead zone.
- A closure keeps access to its creation scope; it explains private state, the var loop bug and stale React state.
- Use map, filter and reduce for transformations, and modern syntax like destructuring, spread and optional chaining.
Practice problems
Predict the output first, then run each snippet in the browser console to check.
- Predict: console.log(typeof null, typeof [], typeof function(){}).
- Predict: console.log(0 == '', null == undefined, NaN === NaN, [] == false).
- Write a once(fn) function that lets fn run only the first time it is called.
- Write a memoize(fn) function using a closure and a Map.
- Fix the var-in-a-loop setTimeout bug in two different ways.
- Implement your own map and filter on Array.prototype (as myMap, myFilter).
- Write a deep clone function, then compare it with structuredClone.
Interview questions and FAQ
What is a closure in JavaScript?
A closure is a function bundled with references to the variables in the scope where it was defined. It can still read and update those variables after the outer function returns, which enables private state, factories and callbacks.
What is the difference between var, let and const?
var is function-scoped and hoisted with the value undefined. let and const are block-scoped and stay in the temporal dead zone until declared. const cannot be reassigned, although objects it points to can still be mutated.
What is hoisting?
JavaScript processes declarations before running code. var variables become available as undefined, function declarations become fully usable, and let, const and class are hoisted but cannot be accessed before their declaration line.
What is the difference between == and ===?
=== compares value and type without conversion. == converts operands to a common type first, which leads to confusing results. Use === except for the deliberate x == null check.
What is the difference between null and undefined?
undefined means a variable has been declared but not given a value, or a property does not exist. null is an intentional "no value" that you assign yourself.
Comments
Post a Comment