2JavaScript Masterclass
What you will master here
- Variables, types, coercion, equality
- Scope (lexical, block, function), hoisting, TDZ
- Closures — what they are, how to spot them, common bugs
this— call-site rules, arrow vs regular, bind/call/apply- Prototypes, classes, inheritance
- Event loop: stack, queues, microtasks, macrotasks
- Promises, async/await, error handling
- Modules (CommonJS vs ESM)
- Iterators, generators, symbols, proxies
- Common pitfalls every interview tests
2.1 Variables: var, let, const
| Keyword | Scope | Hoisted? | Reassignable? |
|---|---|---|---|
var | function | Yes — initialised to undefined | Yes |
let | block | Yes — but TDZ (cannot access) | Yes |
const | block | Yes — TDZ | No (binding); object contents still mutable |
console.log(a); // undefined (var hoists with undefined)
var a = 1;
console.log(b); // ReferenceError — TDZ
let b = 2;
const obj = { x: 1 };
obj.x = 2; // OK — mutating contents
obj = {}; // TypeError — can't reassign binding
2.2 Types and coercion
JS has 7 primitives: string, number, bigint, boolean, undefined, null, symbol. Everything else is an object.
typeof "x" // "string"
typeof 1 // "number"
typeof 1n // "bigint"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" ← legacy bug, can't be fixed
typeof Symbol() // "symbol"
typeof [] // "object"
typeof function(){} // "function" (still an object underneath)
== vs ===
'5' == 5 // true (loose, coerces) '5' === 5 // false (strict) null == undefined // true null === undefined // false NaN == NaN // false (NaN never equals NaN) Number.isNaN(NaN) // true
===. The only legitimate == is x == null as a shortcut for "is null or undefined".Truthy / falsy
Falsy values: false, 0, -0, 0n, '', null, undefined, NaN. Everything else is truthy — including '0', [], {}.
2.3 Scope, hoisting, TDZ
Scope = where a name is visible. Three kinds: global, function, block (let/const). Variables are hoisted to the top of their scope; var initialises to undefined, let/const stay in the Temporal Dead Zone until the declaration line.
// Classic interview trap for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0); // Output: 3 3 3 — all closures share the SAME var for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0); // Output: 0 1 2 — let creates a new binding per iteration
2.4 Closures — the most asked concept
A closure is a function bundled with the variables from the scope where it was defined (not where it's called). The function "closes over" those variables.
function makeCounter() {
let count = 0;
return () => ++count; // inner function closes over `count`
}
const c = makeCounter();
c(); // 1
c(); // 2
c(); // 3
// `count` is private — only the returned function can touch it
Practical uses
- Private state (counters, caches, memoisation)
- Partial application:
const add = a => b => a + b; const inc = add(1); - Event handlers that remember context
- Module pattern (pre-ESM)
Memoise example
function memoize(fn) {
const cache = new Map(); // closed over
return (...args) => {
const k = JSON.stringify(args);
if (cache.has(k)) return cache.get(k);
const v = fn(...args);
cache.set(k, v);
return v;
};
}
const slowSquare = n => (console.log('compute'), n * n);
const fast = memoize(slowSquare);
fast(4); // compute, 16
fast(4); // 16 (no log — cached)
2.5 The this keyword
this is bound by how a function is called, not where it's defined. Five rules:
- Default — bare function call:
this= undefined (strict mode) or global object. - Implicit — method call
obj.fn():this= obj. - Explicit —
fn.call(thisArg, ...),fn.apply(thisArg, [args]),fn.bind(thisArg). - new —
new Fn():this= the new object. - Arrow functions — no own
this; inherit lexically from enclosing scope.
const obj = {
name: 'Alice',
greet() { return `Hi, ${this.name}`; }, // implicit
greetArrow: () => `Hi, ${this?.name}`, // arrow — no this binding
};
obj.greet(); // 'Hi, Alice'
obj.greetArrow(); // 'Hi, undefined' (this is module-level, not obj)
const fn = obj.greet;
fn(); // 'Hi, undefined' — bare call, lost binding
fn.call({ name: 'Bob' }); // 'Hi, Bob' — explicit
class C { constructor() { this.x = 1; } }
const c = new C(); // this = the new instance
2.6 Prototypes & classes
Every object has a hidden link [[Prototype]] pointing to another object (or null). Property lookup walks the chain.
const proto = { greet() { return 'hi'; } };
const obj = Object.create(proto);
obj.greet(); // 'hi' — not on obj, found on proto
class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }
}
class Dog extends Animal {
speak() { return `${this.name} barks`; } // override
bark() { return super.speak() + ' (was bark)'; }
}
new Dog('Rex').speak(); // 'Rex barks'
new Dog('Rex').bark(); // 'Rex makes a sound (was bark)'
Under the hood, class is sugar over prototypes. Dog.prototype.__proto__ === Animal.prototype.
2.7 Event loop — the picture every interview asks for
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1 4 3 2
// Sync first (1, 4), then microtasks (3), then macrotasks (2)
2.8 Promises & async/await
A Promise has 3 states: pending, fulfilled, rejected. Once settled, never changes.
// Creating
const p = new Promise((resolve, reject) => {
setTimeout(() => Math.random() > 0.5 ? resolve('ok') : reject(new Error('bad')), 100);
});
// Consuming with .then / .catch
p.then(v => console.log(v)).catch(e => console.error(e)).finally(() => console.log('done'));
// async/await — same thing, nicer syntax
async function run() {
try {
const v = await p;
console.log(v);
} catch (e) {
console.error(e);
}
}
Combinators
| Method | Behaviour |
|---|---|
Promise.all([a,b,c]) | Resolves when ALL resolve. Rejects on FIRST rejection. |
Promise.allSettled([a,b,c]) | Resolves when all settle. Result is array of {status, value/reason}. |
Promise.race([a,b,c]) | Resolves/rejects on first to settle. |
Promise.any([a,b,c]) | Resolves on first FULFILLED (ignores rejections unless all fail). |
Common bug: forgetting await
async function fetchUser(id) { /* ... */ return user; }
// BUG: returns a Promise, not a user
const u = fetchUser(1);
console.log(u.email); // undefined — u is a Promise
// FIX
const u2 = await fetchUser(1);
console.log(u2.email); // works
2.9 Modules — CommonJS vs ESM
CommonJS (Node.js historic)
// math.js
function add(a,b){return a+b}
module.exports = { add };
// app.js
const { add } = require('./math');
add(1,2);
Synchronous load. Default in older Node, still everywhere.
ES Modules (modern)
// math.js
export function add(a,b){return a+b}
export default function mul(a,b){return a*b}
// app.js
import mul, { add } from './math.js';
add(1,2);
Async load, tree-shakeable, browser-native. Enabled via "type":"module" in package.json or .mjs ext.
2.10 Arrays — methods you must know
// Non-mutating [1,2,3].map(x => x*2); // [2,4,6] [1,2,3].filter(x => x > 1); // [2,3] [1,2,3].reduce((a,b) => a+b, 0); // 6 [1,2,3].some(x => x > 2); // true [1,2,3].every(x => x > 0); // true [1,2,3].find(x => x > 1); // 2 [1,2,3].findIndex(x => x > 1); // 1 [1,2,3].includes(2); // true [1,2,3].flat(); // [1,2,3] [[1,2],[3]].flat(); // [1,2,3] [1,2].concat([3,4]); // [1,2,3,4] [...[1,2], ...[3,4]]; // [1,2,3,4] // Mutating (be careful in functional code) arr.push(x); arr.pop(); arr.unshift(x); arr.shift(); arr.splice(i, n); arr.sort(); arr.reverse();
2.11 Objects — destructuring, spread, copy
const user = { name: 'Alice', age: 30, role: 'admin' };
// Destructure with rename + default
const { name, role: r, level = 1 } = user;
// Spread copies SHALLOW
const copy = { ...user, age: 31 };
// Deep copy
const deep = structuredClone(user);
// or JSON round-trip (loses functions, Dates, Maps...)
const deep2 = JSON.parse(JSON.stringify(user));
// Optional chaining + nullish coalescing
const city = user?.address?.city ?? 'Unknown';
2.12 Iterators, generators, symbols
// Generator — pauseable function
function* range(start, end) {
for (let i = start; i < end; i++) yield i;
}
[...range(0, 5)]; // [0,1,2,3,4]
for (const n of range(0, 3)) console.log(n);
// Symbol — unique key
const s = Symbol('id');
const obj = { [s]: 42 };
obj[s]; // 42 — not iterable in normal for...in
2.13 Common pitfalls (interview gold)
- Reference vs value: objects/arrays passed by reference; primitives by value.
- NaN is not equal to NaN: use
Number.isNaN. - Floating point:
0.1 + 0.2 === 0.3→ false. Use rounding or libraries. - typeof null === 'object': legacy bug, can't fix.
- Array sort defaults to string:
[10,9,1].sort()→ [1,10,9]. Always pass comparator. - for...in vs for...of:
initerates keys (including inherited);ofiterates values of iterables. - Mutating during iteration: don't
pushinto an array you're iterating withforEach. - Async forEach:
arr.forEach(async fn)does NOT await. Usefor...ofwith await, orPromise.all(arr.map(fn)).
Module 18 — JS interview Q&A bank
Difference between let, const and var?
What's a closure?
Why does this print 3 3 3 with var but 0 1 2 with let?
== vs ===?
x == null as a shortcut for null-or-undefined.What are the falsy values?
How does this work in arrow functions?
this. They inherit it from the enclosing lexical scope. That's why arrows are perfect for callbacks but bad for object methods that need this.What are the rules for this in regular functions?
obj.fn() → obj. (3) fn.call/apply/bind(x) → x. (4) new Fn() → newly created object. Arrow functions ignore all of this and use lexical scope.What is the event loop?
Promise.resolve().then(...) runs before setTimeout(..., 0).Difference between Promise.all and Promise.allSettled?
{status, value/reason} — useful when partial success is acceptable.What happens if you forget await?
user.email → undefined). Pure source of bugs; TypeScript catches it with no-floating-promises ESLint rule.How do you await an array of promises?
await Promise.all(arr.map(asyncFn)) runs in parallel. For sequential: for (const x of arr) await asyncFn(x). arr.forEach(async fn) does NOT await — common bug.CommonJS vs ES Modules?
require/module.exports, synchronous, default in legacy Node. ESM uses import/export, async, tree-shakeable, browser-native, modern standard. Enable ESM via "type":"module" or .mjs.How do you copy an object?
{...obj} or Object.assign({}, obj). Deep: structuredClone(obj) (modern, handles Dates/Maps), or JSON round-trip (loses non-JSON types).What is hoisting?
var hoists initialised to undefined; functions hoist entirely; let/const hoist but stay in the TDZ — accessing them before the line throws ReferenceError.What is the prototype chain?
[[Prototype]] link to another object (or null). When you access a property and it isn't on the object, the engine walks the chain looking for it. Classes are sugar over this mechanism — Dog.prototype.__proto__ === Animal.prototype.What's the difference between for...in and for...of?
What's an immediately invoked function expression (IIFE)?
(function(){ ... })() — defines and immediately calls. Used pre-modules to create a private scope. Less needed in modern JS since ESM gives module-level scope automatically.What's the default value of this in strict mode?
undefined for bare function calls in strict mode (vs the global object in sloppy mode). All ES modules are strict by default. Strict mode catches mistakes like leaking variables.Explain debouncing and throttling.
What is structuredClone?
2.14 More JS interview questions
What's a Promise.allSettled use case?
all() rejects on first failure and you lose successful results.Microtask vs macrotask order — give an example.
console.log(1); setTimeout(()=>console.log(2), 0); Promise.resolve().then(()=>console.log(3)); console.log(4); → 1, 4, 3, 2. Sync first, then microtasks drain fully, then one macrotask.What is event delegation?
event.target to handle clicks on children. Reduces listener count for dynamic lists; survives DOM additions/removals without rewiring.Why does typeof null === 'object'?
x === null instead.What's 'use strict'?
with, no duplicate param names. ESM and class bodies are strict by default.What is shallow vs deep equality? How to test?
What is currying?
add(a,b) → add(a)(b). Enables partial application. Implemented with closures.What's a generator function?
function* () { yield x }. Each call resumes from last yield. Used for lazy sequences, iterators, async coroutines (before async/await). for...of consumes.Difference between setTimeout(fn, 0) and queueMicrotask(fn)?
How does new work under the hood?
What's a memory leak in JS — common causes?
What is the difference between map and forEach?
Why is {} + [] 0 in JS?
{} at statement start is treated as a block, leaving + [] which becomes 0. As expression ({} + []) = "[object Object]". JS's coercion rules are a famous footgun — avoid relying on them.How would you implement a one-time event listener?
el.addEventListener('click', fn, { once: true }). Manual: wrap fn so first call removes itself.What does Object.freeze() do? Is it deep?
Difference between Array.from and spread on iterables?
Array.from(iter, mapFn) takes a mapper (one fewer iteration than [...iter].map(...)). Spread doesn't.What's a tagged template literal?
html`<p>${name}</p>`. Used by styled-components, lit-html, SQL escapers.What are getters and setters?
get x() { return this._x; } set x(v) { this._x = v; }. Useful for computed properties, validation on set, lazy values.