Language fundamentals
Questions in this set 10
- 01var vs let vs const — explain hoisting and the temporal dead zone.
- 02What is a closure? Give a real use.
- 03How is this determined?
- 04Explain prototypal inheritance and the prototype chain.
- 05== vs ===, and what does coercion actually do?
- 06null vs undefined vs not-declared.
- 07Explain shallow vs deep copy in JS.
- 08What are map, filter, reduce, and when is reduce the wrong choice?
- 09Explain event bubbling, capturing and delegation.
- 10What is the difference between function declarations and expressions, and what is an IIFE?
var vs let vs const — explain hoisting and the temporal dead zone.
All three are hoisted, but differently. var is function-scoped and initialised to undefined at hoist time. let/const are block-scoped and sit in the temporal dead zone from the top of the block until the declaration line — touching them there throws ReferenceError.
console.log(a); // undefined
console.log(b); // ReferenceError: Cannot access 'b' before initialization
var a = 1;
let b = 2;
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 2That loop is the classic: var has one binding for the whole loop; let creates a fresh binding per iteration. const prevents reassignment, not mutation — const o = {}; o.x = 1 is legal.
Rule to state: default to const, use let when you must reassign, never use var in new code.
What is a closure? Give a real use.
A closure is a function together with the lexical environment it was defined in — the inner function keeps its outer variables alive after the outer call returns.
function rateLimiter(maxPerMinute) {
let hits = []; // private state, captured
return function allow() {
const now = Date.now();
hits = hits.filter(t => now - t < 60_000);
if (hits.length >= maxPerMinute) return false;
hits.push(now);
return true;
};
}
const allow = rateLimiter(5);Real uses: private state (module pattern), memoisation, once-only initialisers, partial application, and every React hook (useState closes over the fiber's slot). The cost: captured variables cannot be GC'd while the closure lives — a common leak when an event handler closes over a large DOM subtree.
Follow-up: "What is a stale closure?" A callback captured an old value and keeps using it — the classic React useEffect bug fixed by adding the dependency or using the functional setter setX(x => x + 1).
How is this determined?
At call time, by how the function is called — except for arrow functions, which have no this and inherit it lexically from where they were defined. Precedence, highest first:
new Fn()→ the new object.fn.call/apply/bind(obj)→ the bound object.obj.fn()→obj.- Plain
fn()→undefinedin strict mode/modules,globalThisotherwise.
const user = {
name: "Ada",
greet() { return `hi ${this.name}`; },
};
const g = user.greet;
g(); // "hi undefined" — the method lost its receiver
g.call(user); // "hi Ada"
setTimeout(() => user.greet(), 0); // fine — arrow preserves the call
setTimeout(user.greet.bind(user), 0); // also fineTrap: an arrow function as an object method captures the enclosing scope, not the object, so this.name is undefined. Use arrows for callbacks, ordinary functions (or class methods) for methods.
Explain prototypal inheritance and the prototype chain.
Every object has an internal [[Prototype]] link (Object.getPrototypeOf(o), historically __proto__). A property lookup walks that chain until it finds the key or hits null. class syntax is sugar over exactly this.
class Animal { speak() { return "..."; } }
class Dog extends Animal { speak() { return "woof"; } }
const d = new Dog();
Object.getPrototypeOf(d) === Dog.prototype; // true
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
d.hasOwnProperty("speak"); // false — it's on the prototypeFn.prototype is the object that instances of Fn will delegate to; obj.__proto__ is the link an existing object already has. Confusing those two is the standard wrong answer.
== vs ===, and what does coercion actually do?
=== compares type and value with no conversion. == applies the abstract equality algorithm: null == undefined is true (and equal to nothing else); string/number comparisons convert the string; objects are converted via valueOf/toString.
0 == ""; // true
0 == "0"; // true
"" == "0"; // false <- not transitive; this is why == is banned in most style guides
null == 0; // false
NaN === NaN; // false (use Number.isNaN or Object.is)
[] == false; // trueAlways use ===. The one accepted exception is x == null as a compact check for "null or undefined".
null vs undefined vs not-declared.
undefined = declared but no value assigned (also: missing argument, missing property, function with no return). null = deliberately empty, assigned by you. Not declared = ReferenceError on access, though typeof missing returns "undefined" without throwing — the one legitimate use of typeof guards.
Relevant modern operators: ?? falls back only on null/undefined (unlike ||, which also fires on 0 and ""), and ?. short-circuits the whole chain.
const port = config.port ?? 3000; // 0 stays 0
const port2 = config.port || 3000; // BUG: 0 becomes 3000
user?.profile?.email; // undefined instead of a TypeErrorExplain shallow vs deep copy in JS.
const copy = { ...obj }; // shallow: nested objects still shared
const copy2 = Object.assign({}, obj); // shallow
const deep = structuredClone(obj); // deep, handles Dates/Maps/Sets/cycles; no functions
const legacy = JSON.parse(JSON.stringify(o)); // deep-ish: loses undefined, Date->string, throws on cyclesstructuredClone is the modern correct answer (Node 17+, all current browsers). Knowing why JSON.parse(JSON.stringify(...)) is wrong — dates, undefined, NaN, Map, cycles — is the actual test.
What are map, filter, reduce, and when is reduce the wrong choice?
map transforms 1:1, filter selects, reduce folds to any shape.
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
const byId = users.reduce((acc, u) => (acc[u.id] = u, acc), {}); // prefer Object.fromEntries
const byId2 = Object.fromEntries(users.map(u => [u.id, u])); // clearer
const grouped = Object.groupBy(users, u => u.role); // ES2024reduce is wrong when a for...of loop reads better, and it is a performance trap when the accumulator is spread each iteration ({...acc, [k]: v} makes it O(n²)). Mutating a local accumulator is fine and fast.
Explain event bubbling, capturing and delegation.
An event travels capture (window → target), hits the target, then bubbles back up. addEventListener(type, fn, true) (or {capture: true}) listens on the way down.
Delegation attaches one listener to a stable ancestor and inspects event.target — essential for lists that change:
list.addEventListener("click", (e) => {
const btn = e.target.closest("[data-id]");
if (!btn || !list.contains(btn)) return;
remove(btn.dataset.id);
});stopPropagation() halts travel; preventDefault() cancels the browser's default action; they are independent. Note focus/blur do not bubble (use focusin/focusout), and passive: true on scroll/touch listeners tells the browser it need not wait for you before scrolling.
What is the difference between function declarations and expressions, and what is an IIFE?
Declarations are fully hoisted (callable before their line); expressions are not (the binding is hoisted, the value is not). An IIFE (() => { … })() creates a scope immediately — historically for privacy, now largely replaced by modules and blocks, though still handy for a top-level await shim in CommonJS.