JavaScript Masterclass

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

KeywordScopeHoisted?Reassignable?
varfunctionYes — initialised to undefinedYes
letblockYes — but TDZ (cannot access)Yes
constblockYes — TDZNo (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
RuleAlways use ===. 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

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:

  1. Default — bare function call: this = undefined (strict mode) or global object.
  2. Implicit — method call obj.fn(): this = obj.
  3. Explicitfn.call(thisArg, ...), fn.apply(thisArg, [args]), fn.bind(thisArg).
  4. newnew Fn(): this = the new object.
  5. 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

Call Stack main() processOrder() currentTask Runs to completion Microtask queue Promise then, queueMicrotask DRAINS COMPLETELY between tasks Higher priority Macrotask queue setTimeout, setInterval I/O, UI events One per loop turn Event Loop if (stack empty) drain microtasks take 1 macrotask
Figure 12 — Microtasks drain fully before the next macrotask runs.
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

MethodBehaviour
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)

Module 18 — JS interview Q&A bank

Difference between let, const and var?
var is function-scoped and hoisted to undefined. let and const are block-scoped and live in the TDZ until declared. const can't be reassigned; let can. Object contents inside const are still mutable.
What's a closure?
A function plus the variables from the scope where it was defined. Inner functions retain access to the outer scope's variables even after the outer function returns. Used for private state, partial application, memoisation.
Why does this print 3 3 3 with var but 0 1 2 with let?
var is function-scoped — all three setTimeout callbacks close over the SAME i. By the time they fire, the loop is done and i is 3. let is block-scoped — each iteration creates a fresh i, so each callback closes over its own.
== vs ===?
=== is strict — same type and same value. == is loose — coerces types. Always use ===. Only legitimate use of == is x == null as a shortcut for null-or-undefined.
What are the falsy values?
false, 0, -0, 0n, '', null, undefined, NaN. Everything else is truthy — including '0', [], {}, 'false'.
How does this work in arrow functions?
Arrow functions don't have their own 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?
(1) Bare call → undefined (strict) or global. (2) Method call 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?
The mechanism that runs JS code, then drains microtasks (Promise.then, queueMicrotask), then picks one macrotask (setTimeout, I/O), then repeats. Microtasks always drain completely before the next macrotask — that's why Promise.resolve().then(...) runs before setTimeout(..., 0).
Difference between Promise.all and Promise.allSettled?
all rejects on the first rejection — useful when you need every result. allSettled waits for all to finish regardless, returns an array of {status, value/reason} — useful when partial success is acceptable.
What happens if you forget await?
You get the Promise back, not the value. Subsequent code may try to use the Promise as if it were the resolved value (e.g. 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?
CommonJS uses 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?
Shallow: {...obj} or Object.assign({}, obj). Deep: structuredClone(obj) (modern, handles Dates/Maps), or JSON round-trip (loses non-JSON types).
What is hoisting?
JS engine "lifts" declarations to the top of their scope before execution. 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?
Every object has a [[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?
for...in iterates enumerable property KEYS (including inherited) — usually for objects. for...of iterates VALUES of an iterable (array, Set, Map, generator). Use of for arrays, never in.
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.
Debounce: wait until N ms after the LAST call to actually fire (e.g. don't search on every keystroke, only when typing stops). Throttle: fire at most once every N ms regardless of call rate (e.g. scroll handlers). Implement with closure + setTimeout.
What is structuredClone?
Built-in deep cloner (since 2022). Handles Dates, Maps, Sets, ArrayBuffer, circular references. Replaces JSON round-trip for deep copies. Doesn't clone functions or DOM nodes.

2.14 More JS interview questions

What's a Promise.allSettled use case?
When you want all results regardless of failures — fetching multiple endpoints, you want to display whichever succeeded. 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?
Attach one listener to a parent; check event.target to handle clicks on children. Reduces listener count for dynamic lists; survives DOM additions/removals without rewiring.
Why does typeof null === 'object'?
Historical bug from JS's earliest design (objects had type tag 000, null was the all-zero pointer). Fixing it would break the web. Use x === null instead.
What's 'use strict'?
Enables strict mode: throws on undeclared vars, makes silent errors loud, this is undefined in functions (not global), no more with, no duplicate param names. ESM and class bodies are strict by default.
What is shallow vs deep equality? How to test?
Shallow: same reference. Deep: same value structure recursively. Test deep: implement recursively or use a library (lodash isEqual, dequal). Custom: same type, same length/keys, recurse on values.
What is currying?
Transform a function of N args into a chain of N functions of 1 arg each. 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)?
setTimeout queues a macrotask (runs after current microtask drain). queueMicrotask queues a microtask (runs before next macrotask). Microtask runs sooner.
How does new work under the hood?
(1) Create empty object. (2) Set its __proto__ to Fn.prototype. (3) Call Fn with this = new object. (4) If Fn returned an object, use it; else use the new object.
What's a memory leak in JS — common causes?
Forgotten timers/intervals, listeners not removed, detached DOM nodes referenced from JS, closures holding large objects, global vars accumulating. Use Chrome DevTools Memory panel; heap snapshot diff between two states reveals retained objects.
What is the difference between map and forEach?
map returns a new array of transformed values. forEach returns undefined (side-effect iteration). Chain map/filter/reduce for functional pipelines; use forEach when no result needed.
Why is {} + [] 0 in JS?
Type coercion zoo. {} 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?
Built-in: el.addEventListener('click', fn, { once: true }). Manual: wrap fn so first call removes itself.
What does Object.freeze() do? Is it deep?
Makes object's own properties read-only. Shallow — nested objects still mutable. For deep freeze, recurse manually.
Difference between Array.from and spread on iterables?
Both convert iterables to arrays. Array.from(iter, mapFn) takes a mapper (one fewer iteration than [...iter].map(...)). Spread doesn't.
What's a tagged template literal?
A function call where the literal's static strings + dynamic values are passed as arrays. html`<p>${name}</p>`. Used by styled-components, lit-html, SQL escapers.
What are getters and setters?
Methods that look like properties: get x() { return this._x; } set x(v) { this._x = v; }. Useful for computed properties, validation on set, lazy values.