Cheatsheet

Interview Handbook

JavaScript Core — Interview Handbook

Deep-dive into JavaScript fundamentals that appear in every frontend interview: types, closures, prototypes, the event loop, promises, ES6+, and performance.

10Core Topics
10Quizzes
42Flashcards
58Code Examples
01 — FOUNDATION

Types, Values & Coercion

Understanding types, values, and coercion is crucial for JavaScript interviews because these concepts are the foundation of how JavaScript operates and are frequently tested. Misunderstandings about primitive vs reference types, coercion rules, and special values like NaN and null can lead to subtle bugs. Mastering these topics demonstrates a deep grasp of the language's core mechanics.

Primitive vs Reference Types

Primitive types (string, number, boolean, null, undefined, symbol, bigint) are immutable and stored by value. Reference types (object, array, function) are mutable and stored by reference. When you assign or compare, primitives copy the value, while references copy the memory address.

JavaScript
let a = 10;
let b = a;
b = 20;
console.log(a); // 10 (primitive, independent)

let obj1 = { value: 10 };
let obj2 = obj1;
obj2.value = 20;
console.log(obj1.value); // 20 (reference, shared)

Type Coercion and ==

JavaScript's == operator performs type coercion before comparing, converting values to a common type. This can lead to unexpected results. Always prefer === (strict equality) to avoid coercion surprises, unless you intentionally need coercion.

JavaScript
console.log(5 == '5');   // true (coercion: string to number)
console.log(5 === '5');  // false (no coercion)
console.log(false == 0); // true (coercion: boolean to number)
console.log(null == undefined); // true (special rule)
Interview Tip

Memorize the coercion table for ==: null and undefined are equal to each other but not to anything else. For other types, JavaScript converts to numbers or strings. In interviews, always explain that === is safer and preferred.

typeof and instanceof

typeof returns a string indicating the type of a value. It's useful for primitives but has quirks (e.g., typeof null returns 'object'). instanceof checks if an object is an instance of a constructor in its prototype chain, working only with objects.

JavaScript
console.log(typeof 42);        // 'number'
console.log(typeof 'hello');   // 'string'
console.log(typeof null);      // 'object' (historical bug)
console.log(typeof undefined); // 'undefined'

console.log([] instanceof Array);   // true
console.log({} instanceof Object);  // true
console.log(5 instanceof Number);   // false (5 is primitive)

null vs undefined

undefined is the default value for uninitialized variables or missing properties. null is an intentional absence of any object value. They are loosely equal (null == undefined is true) but strictly different (null === undefined is false).

JavaScript
let x;
console.log(x); // undefined

let y = null;
console.log(y); // null

console.log(null == undefined);  // true
console.log(null === undefined); // false

NaN

NaN (Not-a-Number) is a special number value resulting from invalid math operations. It is the only value in JavaScript that is not equal to itself (NaN !== NaN). Use Number.isNaN() to reliably check for NaN.

JavaScript
console.log(0 / 0);        // NaN
console.log(NaN === NaN);  // false
console.log(Number.isNaN(NaN)); // true
console.log(isNaN('hello'));    // true (coerces to number)
console.log(Number.isNaN('hello')); // false (no coercion)

BigInt and Symbol

BigInt allows representation of integers larger than Number.MAX_SAFE_INTEGER (2^53 - 1). Symbol creates unique, immutable values often used as object property keys to avoid name collisions.

JavaScript
const big = 9007199254740991n + 1n;
console.log(big); // 9007199254740992n

const sym1 = Symbol('id');
const sym2 = Symbol('id');
console.log(sym1 === sym2); // false (unique)

const obj = { [sym1]: 'value' };
console.log(obj[sym1]); // 'value'
🧩Quick Check

What does the following code output? console.log(typeof NaN);

Flashcards4
02 — FOUNDATION

Scope, Closures & Hoisting

Scope, closures, and hoisting are foundational JavaScript concepts that appear in nearly every technical interview. Understanding how variables are accessed, how functions remember their lexical environment, and how declarations are processed before execution will help you debug tricky code and write more predictable programs.

var vs let vs const

Featurevarletconst
ScopeFunction scopeBlock scopeBlock scope
HoistingHoisted (initialized as undefined)Hoisted (TDZ)Hoisted (TDZ)
ReassignmentAllowedAllowedNot allowed
RedeclarationAllowedNot allowedNot allowed
JavaScript
function example() {
  console.log(a); // undefined (hoisted)
  var a = 5;
  
  console.log(b); // ReferenceError: Cannot access 'b' before initialization
  let b = 10;
  
  const c = 15;
  c = 20; // TypeError: Assignment to constant variable
}

Function vs Block Scope

Variables declared with var are scoped to the nearest function, while let and const are scoped to the nearest block (e.g., if, for, while). This distinction is critical for avoiding unintended variable leakage.

JavaScript
if (true) {
  var x = 10;
  let y = 20;
}
console.log(x); // 10 (function scope, leaks out)
console.log(y); // ReferenceError: y is not defined (block scope)

Hoisting Mechanics

Hoisting moves variable and function declarations to the top of their scope during compilation. var declarations are hoisted and initialized with undefined, while let and const are hoisted but remain in the temporal dead zone (TDZ) until their actual declaration line.

JavaScript
console.log(foo); // undefined (var hoisted)
var foo = 'bar';

console.log(baz); // ReferenceError: Cannot access 'baz' before initialization
let baz = 'qux';

sayHello(); // 'Hello!' (function declaration hoisted)
function sayHello() {
  console.log('Hello!');
}

sayHi(); // TypeError: sayHi is not a function (var hoisted, not initialized)
var sayHi = function() {
  console.log('Hi!');
};

Temporal Dead Zone (TDZ)

The TDZ is the period between entering a scope and the actual declaration of a let or const variable. Accessing the variable during this time throws a ReferenceError. This prevents the common bugs associated with var hoisting.

JavaScript
{
  // TDZ for 'name' starts here
  console.log(name); // ReferenceError
  let name = 'Alice';
  // TDZ ends here
}
Interview Tip

When asked about hoisting, always mention the temporal dead zone for let and const. Interviewers love to see that you understand the difference between hoisting (declaration moved) and initialization (value assignment). A common trick question: 'What does console.log(a) output before let a = 5?' The answer is a ReferenceError, not undefined.

Closures: Definition and Use Cases

A closure is a function that retains access to its lexical scope even when executed outside that scope. Closures are used for data privacy, creating function factories, and maintaining state in asynchronous code.

JavaScript
function createCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// 'count' is private and persists across calls

IIFE Pattern (Immediately Invoked Function Expression)

An IIFE is a function that runs as soon as it is defined. It creates a new scope to avoid polluting the global namespace, often used for data privacy and module patterns before ES6 modules.

JavaScript
(function() {
  var privateVar = 'secret';
  console.log(privateVar); // 'secret'
})();

console.log(typeof privateVar); // 'undefined' (private)

// Modern alternative using block scope:
{
  let privateVar = 'secret';
  console.log(privateVar);
}
🧩Quick Check

What will be the output of the following code? for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); }

Flashcards4
03 — FOUNDATION

Functions, this & Execution

Functions are the building blocks of JavaScript, and mastering how they work—especially the this keyword—is critical for acing interviews. This section covers function declarations vs expressions, arrow vs regular functions, this binding rules, call/apply/bind, the arguments object vs rest parameters, and currying. Understanding these concepts will help you write cleaner, more predictable code and answer common interview questions with confidence.

Function Declarations vs Expressions

A function declaration is hoisted to the top of its scope, meaning you can call it before its definition. A function expression is not hoisted—it's assigned to a variable and can only be used after the assignment. Use declarations for named functions you want to hoist; use expressions for anonymous functions or when you need to assign them conditionally.

JavaScript
// Function declaration (hoisted)
console.log(add(2, 3)); // 5
function add(a, b) {
  return a + b;
}

// Function expression (not hoisted)
console.log(subtract(5, 2)); // ReferenceError
const subtract = function(a, b) {
  return a - b;
};

Arrow vs Regular Functions

Arrow functions are syntactically shorter and do not have their own this, arguments, or super—they inherit this from the enclosing lexical scope. Regular functions have their own this based on how they are called. Use arrow functions for callbacks or when you want to preserve the outer this; use regular functions when you need dynamic this binding or the arguments object.

JavaScript
const obj = {
  name: 'Alice',
  regularFunc: function() {
    console.log(this.name); // 'Alice' (own this)
  },
  arrowFunc: () => {
    console.log(this.name); // undefined (inherits from outer scope)
  }
};
obj.regularFunc();
obj.arrowFunc();

this Binding Rules

The value of this is determined by how a function is called, not where it's defined. There are four main rules: default binding (global object or undefined in strict mode), implicit binding (object method call), explicit binding (call/apply/bind), and new binding (constructor call). Arrow functions ignore these rules and use lexical scoping.

JavaScript
function showThis() {
  console.log(this);
}

// Default binding (non-strict mode)
showThis(); // Window (or global)

// Implicit binding
const obj = { name: 'Bob', show: showThis };
obj.show(); // { name: 'Bob', show: f }

// Explicit binding
showThis.call({ name: 'Charlie' }); // { name: 'Charlie' }

// New binding
new showThis(); // showThis {}
Interview Tip: this in Event Handlers

In DOM event handlers, this refers to the element that fired the event when using a regular function. With an arrow function, this comes from the surrounding lexical context (e.g., the class instance). Interviewers often ask about this difference—be ready to explain it with an example.

call, apply, and bind

call and apply invoke a function immediately with a specified this value. call takes arguments individually; apply takes an array of arguments. bind returns a new function with a permanently bound this (and optional partial arguments) that can be called later. These are essential for borrowing methods and setting context.

JavaScript
const person = {
  name: 'Dave',
  greet: function(greeting, punctuation) {
    console.log(greeting + ', ' + this.name + punctuation);
  }
};

const otherPerson = { name: 'Eve' };

person.greet.call(otherPerson, 'Hello', '!'); // Hello, Eve!
person.greet.apply(otherPerson, ['Hi', '?']); // Hi, Eve?

const boundGreet = person.greet.bind(otherPerson, 'Hey');
boundGreet('.'); // Hey, Eve.

arguments Object vs Rest Parameters

The arguments object is an array-like object available inside regular functions (not arrow functions) that contains all passed arguments. Rest parameters (...args) are a modern alternative that provides a real array. Use rest parameters for cleaner code and array methods; avoid arguments unless you need backward compatibility.

JavaScript
// arguments object (regular function only)
function sumArgs() {
  let total = 0;
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}
console.log(sumArgs(1, 2, 3)); // 6

// Rest parameters (works in arrow functions too)
const sumRest = (...numbers) => numbers.reduce((acc, n) => acc + n, 0);
console.log(sumRest(1, 2, 3)); // 6

Currying

Currying transforms a function that takes multiple arguments into a sequence of functions each taking a single argument. It's useful for partial application and creating reusable, composable functions. In JavaScript, currying is often implemented manually or with libraries like Lodash.

JavaScript
// Manual currying
function multiply(a) {
  return function(b) {
    return a * b;
  };
}

const double = multiply(2);
console.log(double(5)); // 10

// Arrow function currying
const add = a => b => a + b;
const add5 = add(5);
console.log(add5(3)); // 8
🧩Quick Check

What will the following code log?

Flashcards5
04 — CORE

Prototypes & Inheritance

Prototypes and inheritance are foundational to how JavaScript objects work. Interviewers frequently test your understanding of the prototype chain, Object.create, and how modern class syntax relates to prototypal inheritance. Mastering these concepts shows you grasp JavaScript's unique object model, not just syntactic sugar.

The Prototype Chain

Every JavaScript object has an internal link to another object called its prototype. When you access a property on an object, JavaScript first checks the object itself, then walks up the prototype chain until it finds the property or reaches null. This is how inheritance works in JavaScript.

JavaScript
const animal = { eats: true };
const dog = { barks: true };
Object.setPrototypeOf(dog, animal);

console.log(dog.eats); // true (inherited from animal)
console.log(dog.barks); // true (own property)
console.log(dog.hasOwnProperty('eats')); // false

Object.create

Object.create(proto) creates a new object with the specified prototype. It's a clean way to set up inheritance without constructor functions. You can also pass a properties descriptor object as the second argument.

JavaScript
const vehicle = {
  start() { return 'Engine started'; }
};

const car = Object.create(vehicle);
car.drive = () => 'Driving';

console.log(car.start()); // 'Engine started'
console.log(Object.getPrototypeOf(car) === vehicle); // true

Class Syntax Under the Hood

ES6 class is syntactic sugar over the existing prototype-based inheritance. A class's methods are stored on ClassName.prototype, and instances have a __proto__ link to that prototype. The constructor method is called when you use new.

JavaScript
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  speak() {
    return `${this.name} barks`;
  }
}

const d = new Dog('Rex');
console.log(d.speak()); // 'Rex barks'
console.log(Object.getPrototypeOf(d) === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true

Mixin Patterns

Since JavaScript only supports single inheritance through the prototype chain, mixins allow you to compose behavior from multiple sources. A mixin is an object with methods that you copy into another object's prototype.

JavaScript
const canFly = {
  fly() { return `${this.name} is flying`; }
};

const canSwim = {
  swim() { return `${this.name} is swimming`; }
};

class Bird {
  constructor(name) { this.name = name; }
}

// Apply mixins
Object.assign(Bird.prototype, canFly, canSwim);

const duck = new Bird('Duck');
console.log(duck.fly());  // 'Duck is flying'
console.log(duck.swim()); // 'Duck is swimming'

instanceof and Prototype Checks

The instanceof operator checks if the prototype property of a constructor appears anywhere in an object's prototype chain. For more precise checks, use Object.getPrototypeOf or isPrototypeOf.

JavaScript
function Car() {}
const myCar = new Car();

console.log(myCar instanceof Car); // true
console.log(myCar instanceof Object); // true

// Manual check
console.log(Car.prototype.isPrototypeOf(myCar)); // true
console.log(Object.getPrototypeOf(myCar) === Car.prototype); // true

Object.getPrototypeOf

Object.getPrototypeOf(obj) returns the prototype of a given object. It's the standard way to get the prototype (preferred over the deprecated __proto__). Use it to inspect or traverse the prototype chain.

JavaScript
const base = { x: 1 };
const derived = Object.create(base);

console.log(Object.getPrototypeOf(derived)); // { x: 1 }
console.log(Object.getPrototypeOf(derived) === base); // true

// Traverse chain
let proto = Object.getPrototypeOf(derived);
while (proto) {
  console.log(proto);
  proto = Object.getPrototypeOf(proto);
}
Interview Tip: Distinguish Own vs Inherited Properties

Interviewers often ask how to tell if a property is on the object itself or inherited. Use hasOwnProperty (or Object.hasOwn in modern JS) to check. For example: obj.hasOwnProperty('toString') returns false because toString is inherited from Object.prototype.

🧩Quick Check

What does the following code log?

Flashcards4
05 — CORE

Event Loop & Asynchrony

The event loop is the core mechanism that enables JavaScript's non-blocking concurrency despite being single-threaded. Interviewers frequently probe this topic to assess your understanding of asynchronous execution order, which is critical for debugging race conditions and optimizing UI performance. Mastering the event loop distinguishes junior from senior developers.

Call Stack

The call stack is a LIFO (Last In, First Out) data structure that tracks function execution. When a function is called, it's pushed onto the stack; when it returns, it's popped off. The event loop can only process tasks when the call stack is empty.

JavaScript
function foo() {
  console.log('foo');
  bar();
}
function bar() {
  console.log('bar');
}
foo();
// Stack: foo -> bar (then pop bar, pop foo)
// Output: foo, bar

Task Queue vs Microtask Queue

The task queue (macrotask queue) holds callbacks from setTimeout, setInterval, and I/O events. The microtask queue holds promises, queueMicrotask, and MutationObserver callbacks. After each macrotask, the event loop empties the entire microtask queue before processing the next macrotask.

JavaScript
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2
// Explanation: 1 and 4 are sync, then microtask (3) runs before macrotask (2)
Interview Tip: Microtask Starvation

If you recursively enqueue microtasks (e.g., a promise chain that never resolves), the microtask queue never empties, blocking macrotasks like rendering or setTimeout. This is a common performance pitfall—always ensure microtasks eventually yield.

setTimeout / setInterval Ordering

setTimeout(fn, 0) does not execute immediately; it schedules the callback as a macrotask with a minimum delay of 0ms (browsers clamp to 4ms after nesting). setInterval similarly queues callbacks, but if execution takes longer than the interval, callbacks can stack.

JavaScript
let count = 0;
const id = setInterval(() => {
  console.log(count++);
  if (count === 3) clearInterval(id);
}, 100);
// Output: 0, 1, 2 (each ~100ms apart)

requestAnimationFrame

requestAnimationFrame schedules a callback before the next browser repaint, making it ideal for animations. It runs after microtasks but before the next macrotask, and is tied to the display refresh rate (typically 60fps).

JavaScript
let start = null;
function animate(timestamp) {
  if (!start) start = timestamp;
  const progress = timestamp - start;
  element.style.transform = `translateX(${Math.min(progress / 10, 200)}px)`;
  if (progress < 2000) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

queueMicrotask

queueMicrotask explicitly adds a function to the microtask queue. It's useful for deferring work until after the current synchronous execution but before any macrotasks, such as batching DOM updates.

JavaScript
console.log('sync');
queueMicrotask(() => console.log('microtask'));
console.log('sync end');
// Output: sync, sync end, microtask

Event Loop in Node.js vs Browser

Both environments use the same event loop concept, but Node.js has additional phases: timers, I/O callbacks, idle/prepare, poll, check (setImmediate), and close callbacks. The browser's event loop prioritizes rendering and user interactions, while Node.js focuses on I/O and timers.

FeatureBrowserNode.js
Microtask executionAfter each macrotaskAfter each phase
requestAnimationFrameYes, before repaintNot available
setImmediateNot availableYes, in check phase
I/O handlingEvent-driven (e.g., fetch)libuv thread pool
🧩Quick Check

What is the output of the following code? console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D');

Flashcards4
06 — CORE

Promises & Async/Await

Promises and async/await are fundamental to modern JavaScript, especially for handling asynchronous operations like API calls and file I/O. Interviewers frequently test your understanding of promise states, chaining, and error handling to gauge your ability to write robust, non-blocking code. Mastery of these concepts is essential for senior-level roles and is a common topic in technical screens.

Promise States

A Promise has three states: pending, fulfilled, and rejected. Once settled (fulfilled or rejected), it cannot change state. This immutability is key for reliable async code.

JavaScript
const promise = new Promise((resolve, reject) => {
  // pending
  setTimeout(() => resolve('done'), 1000);
});
console.log(promise); // Promise { <pending> }
promise.then(value => console.log(value)); // 'done' after 1s

Promise Chaining

Chaining allows sequential async operations. Each .then() returns a new promise, enabling clean composition. Always return a value or promise from a .then() to continue the chain.

JavaScript
fetch('/api/user')
  .then(res => res.json())
  .then(user => fetch(`/api/posts?userId=${user.id}`))
  .then(res => res.json())
  .then(posts => console.log(posts))
  .catch(err => console.error(err));

Promise.all / race / allSettled / any

These static methods handle multiple promises: Promise.all rejects fast on any error; Promise.race settles on first settled promise; Promise.allSettled waits for all to settle (never rejects); Promise.any resolves on first fulfillment, rejects only if all reject.

JavaScript
const p1 = Promise.resolve(1);
const p2 = Promise.reject('err');
const p3 = new Promise(resolve => setTimeout(() => resolve(3), 100));

Promise.allSettled([p1, p2, p3]).then(results => {
  console.log(results);
  // [{status:'fulfilled', value:1}, {status:'rejected', reason:'err'}, {status:'fulfilled', value:3}]
});
Interview Tip: Error Handling in Promise.all

Remember that Promise.all fails fast—if any promise rejects, the entire promise rejects immediately. Use Promise.allSettled when you need results from all promises regardless of failures, such as batch API calls where partial success is acceptable.

Async/Await Internals

An async function always returns a promise. await pauses execution until the awaited promise settles, then resumes the function. Under the hood, it's syntactic sugar over generators and promises, managed by the event loop.

JavaScript
async function fetchData() {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
}
// Equivalent to:
// function fetchData() { return fetch('/api/data').then(res => res.json()); }

Error Handling with try/catch

Use try/catch blocks to handle errors in async functions. Uncaught rejections in async functions result in unhandled promise rejections. Always wrap await calls in try/catch or attach a .catch() to the returned promise.

JavaScript
async function getUser(id) {
  try {
    const user = await fetch(`/api/users/${id}`);
    if (!user.ok) throw new Error('User not found');
    return await user.json();
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw error; // re-throw if needed
  }
}

Common Async Pitfalls

  • Forgetting to await inside an async function (returns a promise, not the value).
  • Using forEach with async callbacks (doesn't wait for promises); use for...of or Promise.all instead.
  • Not handling errors in promise chains (missing .catch() or try/catch).
  • Mixing callbacks and promises (leads to callback hell or unhandled rejections).
  • Assuming Promise.all runs promises in sequence (it runs them concurrently).
🧩Quick Check

What does the following code log?

Flashcards4
07 — MODERN JS

ES6+ & Modern Syntax

Modern JavaScript (ES6+) features are essential for writing cleaner, more efficient code and are frequently tested in interviews. Mastering destructuring, spread/rest, template literals, optional chaining, nullish coalescing, generators, iterators, and weak collections demonstrates a deep understanding of the language's evolution. This section covers the most commonly asked topics with practical examples.

Destructuring

Destructuring allows you to unpack values from arrays or properties from objects into distinct variables. It simplifies code and reduces repetition, especially when working with function returns or API responses.

JavaScript
// Array destructuring
const [first, second] = [10, 20];
console.log(first); // 10

// Object destructuring with renaming
const user = { name: 'Alice', age: 30 };
const { name: userName, age } = user;
console.log(userName); // 'Alice'

Spread and Rest Operators

The spread operator (...) expands an iterable into individual elements, while the rest operator collects multiple elements into an array. They are used for copying arrays/objects, merging, and handling variable arguments.

JavaScript
// Spread: copy and merge arrays
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4]; // [1, 2, 3, 4]

// Rest: collect function arguments
function sum(...numbers) {
  return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3)); // 6

Template Literals

Template literals use backticks (`) and support embedded expressions via ${}. They make string interpolation and multi-line strings much more readable.

JavaScript
const name = 'Bob';
const greeting = `Hello, ${name}!`;
console.log(greeting); // 'Hello, Bob!'

const multiLine = `This is
a multi-line
string.`;

Optional Chaining and Nullish Coalescing

Optional chaining (?.) safely accesses nested properties without throwing an error if a reference is null or undefined. Nullish coalescing (??) returns the right-hand operand only when the left is null or undefined (not for other falsy values).

JavaScript
const user = { profile: { name: 'Alice' } };
console.log(user?.profile?.name); // 'Alice'
console.log(user?.address?.city); // undefined

const value = 0;
const result = value ?? 'default';
console.log(result); // 0 (not 'default')
Interview Tip

When asked about optional chaining, emphasize that it short-circuits—if a property is null/undefined, the entire chain returns undefined without evaluating further. This prevents runtime errors in deeply nested data.

Generators and Iterators

Generators are functions that can be paused and resumed using function* and yield. They return an iterator object. Iterators implement the next() method and are used with for...of loops.

JavaScript
function* idGenerator() {
  let id = 1;
  while (true) {
    yield id++;
  }
}
const gen = idGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2

// Custom iterator
const range = {
  from: 1, to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const end = this.to;
    return {
      next() {
        return current <= end
          ? { value: current++, done: false }
          : { done: true };
      }
    };
  }
};
for (const num of range) console.log(num); // 1, 2, 3

WeakMap, WeakSet, and WeakRef

WeakMap and WeakSet hold weak references to objects, meaning they don't prevent garbage collection. They are useful for private data or caching without memory leaks. WeakRef (ES2021) allows holding a weak reference to an object without preventing its collection.

JavaScript
// WeakMap: keys must be objects
const wm = new WeakMap();
let obj = {};
wm.set(obj, 'private data');
obj = null; // obj can be garbage collected

// WeakSet: similar, stores objects weakly
const ws = new WeakSet();
let obj2 = {};
ws.add(obj2);
obj2 = null; // obj2 can be garbage collected

// WeakRef (ES2021)
let ref = new WeakRef({});
console.log(ref.deref()); // object or undefined if collected
🧩Quick Check

What does the nullish coalescing operator (??) return for the expression: `null ?? 'default'`?

Flashcards4
08 — MODERN JS

Modules & the Build Pipeline

This section covers the module system and build pipeline—critical knowledge for any JavaScript interview. You'll need to understand the differences between ES modules and CommonJS, how bundlers like Webpack and Rollup work, and modern features like tree shaking and top-level await. Mastering these topics shows you can write maintainable, performant code in real-world projects.

ES Modules vs CommonJS

ES modules (ESM) are the official standard for JavaScript modules, using import/export syntax. CommonJS (CJS) is Node.js's original module system, using require()/module.exports. Key differences: ESM is static (imports are hoisted and analyzed at parse time), while CJS is dynamic (require can be called conditionally). ESM supports tree shaking; CJS does not.

JavaScript (ESM)
// ES module (ESM)
// math.mjs
export const add = (a, b) => a + b;
export default function multiply(a, b) { return a * b; }

// app.mjs
import multiply, { add } from './math.mjs';
console.log(add(2, 3)); // 5
console.log(multiply(2, 3)); // 6
JavaScript (CJS)
// CommonJS (CJS)
// math.js
const add = (a, b) => a + b;
function multiply(a, b) { return a * b; }
module.exports = { add, multiply };

// app.js
const { add, multiply } = require('./math.js');
console.log(add(2, 3)); // 5
console.log(multiply(2, 3)); // 6

Static vs Dynamic Import

Static imports (import ... from 'module') are evaluated at parse time, enabling optimizations like tree shaking. Dynamic imports (import('module')) return a promise and allow lazy loading at runtime. Use dynamic imports for code splitting or conditionally loading modules.

JavaScript
// Static import (compile-time)
import { format } from 'date-fns';

// Dynamic import (runtime)
const loadChart = async () => {
  const { Chart } = await import('chart.js');
  new Chart(ctx, config);
};

// Conditional dynamic import
if (user.isAdmin) {
  const adminModule = await import('./admin.js');
  adminModule.init();
}

Tree Shaking

Tree shaking is a dead-code elimination technique performed by bundlers (like Webpack, Rollup) that removes unused exports. It relies on static analysis of ES module imports. Only works with ESM, not CJS. To maximize tree shaking, avoid side effects in modules and use named exports.

JavaScript
// utils.js
export const used = () => 'I am used';
export const unused = () => 'I am dead code'; // will be removed

// app.js
import { used } from './utils.js';
console.log(used()); // Only 'used' survives bundling
Interview Tip

When asked about tree shaking, mention that bundlers mark modules with sideEffects: false in package.json to safely remove unused code. Also note that dynamic imports can break tree shaking because the module is loaded at runtime.

Circular Dependency Pitfalls

Circular dependencies occur when two modules import each other. In CJS, this can lead to undefined values because module.exports is not fully populated at the time of require. ESM handles this better with live bindings, but still requires careful design. Best practice: refactor to avoid cycles.

JavaScript (CJS)
// CJS circular dependency example
// a.js
const b = require('./b.js');
console.log(b); // undefined (if b.js requires a.js first)
module.exports = { value: 'A' };

// b.js
const a = require('./a.js');
console.log(a); // { value: 'A' }
module.exports = { value: 'B' };

Bundler Basics

Bundlers like Webpack, Rollup, and Parcel take multiple JavaScript files and combine them into optimized bundles for the browser. They handle module resolution, code splitting, minification, and asset processing. Key concepts: entry point, output, loaders (for non-JS files), and plugins (for advanced transformations).

JavaScript (Webpack)
// Simple Webpack config (webpack.config.js)
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
    ],
  },
  mode: 'production',
};

Top-Level Await

Top-level await allows using await outside of async functions in ES modules. It blocks the module's execution until the promise resolves, making it easier to initialize modules with async dependencies. Supported in modern browsers and Node.js (ESM only).

JavaScript (ESM)
// db.mjs
import { connect } from 'db-driver';
export const db = await connect('mongodb://localhost:27017');

// app.mjs
import { db } from './db.mjs';
// db is ready to use immediately
const data = await db.find('users');
🧩Quick Check

Which of the following is true about tree shaking?

Flashcards4
09 — ADVANCED

Patterns & Functional Techniques

Patterns and functional techniques are core to writing maintainable, scalable JavaScript. Interviewers frequently test your understanding of these patterns to gauge your ability to structure code, manage state, and optimize performance. Mastering these topics demonstrates both theoretical knowledge and practical problem-solving skills.

Module Pattern

The module pattern encapsulates private state and exposes only a public API, preventing global scope pollution. It leverages closures to create private variables and methods.

JavaScript
const CounterModule = (function() {
  let count = 0; // private

  return {
    increment() { count++; },
    decrement() { count--; },
    getCount() { return count; }
  };
})();

CounterModule.increment();
console.log(CounterModule.getCount()); // 1
console.log(CounterModule.count); // undefined (private)

Observer / Pub-Sub Pattern

The Observer pattern allows objects (subscribers) to listen for events on another object (subject). Pub-Sub (Publish-Subscribe) decouples this further with a message broker. Both are essential for event-driven architectures.

JavaScript
class EventBus {
  constructor() {
    this.listeners = {};
  }

  subscribe(event, callback) {
    if (!this.listeners[event]) this.listeners[event] = [];
    this.listeners[event].push(callback);
    return () => this.unsubscribe(event, callback);
  }

  publish(event, data) {
    (this.listeners[event] || []).forEach(cb => cb(data));
  }

  unsubscribe(event, callback) {
    this.listeners[event] = (this.listeners[event] || []).filter(cb => cb !== callback);
  }
}

const bus = new EventBus();
const unsub = bus.subscribe('userLogin', (user) => console.log(`Welcome ${user}`));
bus.publish('userLogin', 'Alice'); // Welcome Alice
unsub();
bus.publish('userLogin', 'Bob'); // no output

Debounce vs Throttle

Debounce delays execution until after a pause in calls, while throttle ensures execution at most once per interval. Use debounce for search inputs, throttle for scroll/resize events.

JavaScript
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

function throttle(fn, limit) {
  let inThrottle = false;
  return function(...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Usage
const log = () => console.log('called');
const debouncedLog = debounce(log, 300);
const throttledLog = throttle(log, 300);
Interview Tip

When implementing debounce or throttle, always handle the this context correctly using fn.apply(this, args). Interviewers often check for this subtlety.

Memoization

Memoization caches function results based on arguments, avoiding redundant computations for pure functions. It's a classic optimization technique.

JavaScript
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const factorial = memoize(function(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
});

console.log(factorial(5)); // 120 (computed)
console.log(factorial(5)); // 120 (cached)

Factory vs Constructor

Constructors (with new) create instances linked to a prototype, while factories return any object without new. Factories offer more flexibility (closures, conditionals) but lack prototype inheritance.

JavaScript
// Constructor
function Car(make, model) {
  this.make = make;
  this.model = model;
}
Car.prototype.drive = function() { console.log('Driving'); };
const car1 = new Car('Toyota', 'Camry');

// Factory
function createCar(make, model) {
  return {
    make,
    model,
    drive() { console.log('Driving'); }
  };
}
const car2 = createCar('Honda', 'Civic');

console.log(car1 instanceof Car); // true
console.log(car2 instanceof Car); // false

Immutability Patterns

Immutability prevents unintended side effects by creating new objects/arrays instead of mutating existing ones. Use Object.assign, spread operator, or libraries like Immer.

JavaScript
const state = { user: { name: 'Alice', age: 30 }, items: [1, 2, 3] };

// Mutating (bad)
state.user.age = 31;

// Immutable update
const newState = {
  ...state,
  user: { ...state.user, age: 31 },
  items: [...state.items, 4]
};

console.log(state.user.age); // 30 (unchanged)
console.log(newState.user.age); // 31
🧩Quick Check

Which pattern is best for decoupling components in an event-driven system?

Flashcards5
10 — ADVANCED

Memory Management & Performance

Memory management and performance are critical topics in JavaScript interviews, as they test your understanding of how the engine handles resources under the hood. Interviewers often probe these areas to gauge your ability to write efficient, leak-free code in production. This section covers garbage collection, common memory leaks, and profiling techniques to help you stand out.

Garbage Collection: Mark-and-Sweep

JavaScript uses automatic garbage collection, primarily via the mark-and-sweep algorithm. It starts from root objects (e.g., global object, local variables) and marks all reachable objects, then sweeps away unmarked ones. This is why unreferenced objects are eventually freed.

JavaScript
// Object becomes unreachable after function returns
function createUser() {
  let user = { name: 'Alice' };
  return user;
}
let userRef = createUser();
userRef = null; // Now eligible for GC

Common Causes of Memory Leaks

Memory leaks occur when objects are no longer needed but remain referenced, preventing garbage collection. Common causes include: global variables, forgotten timers, detached DOM nodes, and closures that retain large data.

  • Global variables: Accidental globals (e.g., undeclared variable) persist forever.
  • Timers: setInterval/setTimeout with references to DOM or large objects.
  • Detached DOM: JavaScript holding references to removed DOM elements.
  • Closures: Functions that capture outer scope variables, preventing their release.

Closure-Based Leaks

Closures can cause memory leaks when they capture large objects or DOM references that outlive their intended use. For example, an event listener inside a closure may keep a reference to a whole scope.

JavaScript
function setupButton() {
  let largeData = new Array(1000000).fill('leak');
  document.getElementById('btn').addEventListener('click', function() {
    console.log('clicked'); // closure retains largeData
  });
}
// largeData persists as long as the listener exists
Interview Tip

When asked about closure leaks, mention that modern engines optimize by not retaining unused variables, but eval or with can break this. Always nullify references when removing listeners.

WeakMap for Private Data

WeakMap holds weak references to keys, meaning it doesn't prevent garbage collection if no other references exist. This is ideal for storing private data without causing memory leaks.

JavaScript
const privateData = new WeakMap();
class User {
  constructor(name) {
    privateData.set(this, { name });
  }
  getName() {
    return privateData.get(this).name;
  }
}
let user = new User('Bob');
user = null; // privateData entry is GC'd automatically

Performance.now and Profiling

Use performance.now() for high-resolution timing (microseconds) to measure code execution. For deeper profiling, browser DevTools (Performance tab) can record memory allocations and identify bottlenecks.

JavaScript
const start = performance.now();
for (let i = 0; i < 1000000; i++) {
  Math.sqrt(i);
}
const end = performance.now();
console.log(`Loop took ${end - start} ms`);

V8 Hidden Classes

V8 optimizes object property access using hidden classes (also called maps). When you add properties in a consistent order, objects share the same hidden class, enabling faster property lookups. Changing property order or deleting properties can cause deoptimization.

JavaScript
// Efficient: same property order
function Point(x, y) {
  this.x = x;
  this.y = y;
}
let p1 = new Point(1, 2);
let p2 = new Point(3, 4); // shares hidden class

// Inefficient: different order
function BadPoint(x, y) {
  this.y = y;
  this.x = x; // different order
}
🧩Quick Check

Which of the following is NOT a common cause of memory leaks in JavaScript?

Flashcards4