Skip to content

JavaScript Fundamentals

Quick Reference

Scan this table in 5 minutes before your interview.

TopicKey PointsCommon Interview Question
ClosuresFunction + its lexical environment. Inner function retains access to outer scope even after outer returns.What will setTimeout inside a for loop with var print? How do you fix it?
Promises3 states: pending, fulfilled, rejected. Promise.all fails fast; Promise.allSettled never short-circuits; Promise.race = first settled; Promise.any = first fulfilled.Implement Promise.all from scratch.
CurryingTransform f(a, b, c) into f(a)(b)(c). Enables partial application and function specialization.Write a sum function where sum(1)(2)(3)() returns 6.
PolyfillsKnow map, filter, reduce, bind, debounce, throttle internals. Respect thisArg, handle edge cases.Implement Array.prototype.reduce from scratch.
this KeywordPrecedence: new > explicit (call/apply/bind) > implicit (method call) > default (global/undefined). Arrow functions use lexical this.What does this refer to in each scenario?
Prototypal InheritanceEvery object has a [[Prototype]]. The new keyword: create obj, set proto, call constructor, return. ES6 class is syntactic sugar.Explain how the new keyword works step by step.
Hoisting & TDZvar declarations hoisted (not value). Function declarations fully hoisted. let/const hoisted but in Temporal Dead Zone until declaration.What is the output of accessing a let variable before its declaration?

1. Closures

What Is a Closure?

A closure is a function bundled together with references to its surrounding lexical environment. When a function is created, it "closes over" the variables in its outer scope, retaining access to them even after the outer function has returned.

js
function createCounter() {
  let count = 0; // closed over by the returned function

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

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// `count` is not accessible directly — true data privacy

Scope Chain

When a variable is referenced, the engine walks up the scope chain:

Current Function Scope → Outer Function Scope → ... → Global Scope
js
const global = "global";

function outer() {
  const outerVar = "outer";

  function middle() {
    const middleVar = "middle";

    function inner() {
      // inner can see: middleVar, outerVar, global
      console.log(middleVar, outerVar, global);
    }

    inner();
  }

  middle();
}

outer(); // "middle" "outer" "global"

Classic Gotcha: Closure in Loops

The problem with var:

js
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3
// All callbacks share the same `i` (function-scoped)

Fix 1 — Use let (block-scoped):

js
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2
// Each iteration gets its own `i`

Fix 2 — IIFE to capture the value:

js
for (var i = 0; i < 3; i++) {
  (function (j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}
// Output: 0, 1, 2

Practical Uses

Data Privacy (Module Pattern):

ts
function createWallet(initialBalance: number) {
  let balance = initialBalance; // private

  return {
    deposit(amount: number) {
      balance += amount;
      return balance;
    },
    withdraw(amount: number) {
      if (amount > balance) throw new Error("Insufficient funds");
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    },
  };
}

const wallet = createWallet(1000);
wallet.deposit(500);   // 1500
wallet.withdraw(200);  // 1300
// wallet.balance → undefined (private!)

Factory Functions:

ts
function createMultiplier(factor: number) {
  return (num: number) => num * factor;
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

Partial Application:

ts
function createApiUrl(baseUrl: string) {
  return (endpoint: string) => {
    return (id?: string) => {
      const url = `${baseUrl}${endpoint}`;
      return id ? `${url}/${id}` : url;
    };
  };
}

const paytmApi = createApiUrl("https://api.paytm.com");
const usersEndpoint = paytmApi("/users");

console.log(usersEndpoint());       // "https://api.paytm.com/users"
console.log(usersEndpoint("123"));  // "https://api.paytm.com/users/123"

2. Promises Deep Dive

Promise States

A Promise is always in one of three states:

  • Pending — initial state, neither fulfilled nor rejected.
  • Fulfilled — the operation completed successfully.
  • Rejected — the operation failed.

Once settled (fulfilled or rejected), a promise never changes state.

js
const p = new Promise((resolve, reject) => {
  // pending here
  setTimeout(() => resolve("done"), 1000); // → fulfilled
});

p.then((val) => console.log(val));   // "done"
p.catch((err) => console.log(err));  // not called

Promise Combinators

Promise.all

All must resolve. Fails fast on the first rejection.

ts
const results = await Promise.all([
  fetch("/api/user"),
  fetch("/api/transactions"),
  fetch("/api/balance"),
]);
// If any one rejects, the whole thing rejects immediately

Promise.allSettled

Waits for all promises to settle. Never short-circuits. Returns an array of { status, value/reason }.

ts
const results = await Promise.allSettled([
  Promise.resolve("OK"),
  Promise.reject("Fail"),
  Promise.resolve("Also OK"),
]);

// [
//   { status: "fulfilled", value: "OK" },
//   { status: "rejected",  reason: "Fail" },
//   { status: "fulfilled", value: "Also OK" }
// ]

Promise.race

First promise to settle (fulfill or reject) wins.

ts
const result = await Promise.race([
  fetch("/api/primary"),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Timeout")), 5000)
  ),
]);
// Useful for implementing request timeouts

Promise.any

First promise to fulfill wins. Ignores rejections unless all reject (then throws AggregateError).

ts
const fastest = await Promise.any([
  fetch("https://cdn1.paytm.com/bundle.js"),
  fetch("https://cdn2.paytm.com/bundle.js"),
  fetch("https://cdn3.paytm.com/bundle.js"),
]);
// Returns the first successful response; ignores failed CDNs

Polyfill: Promise.all

ts
function promiseAll<T>(promises: Array<T | Promise<T>>): Promise<T[]> {
  return new Promise((resolve, reject) => {
    const results: T[] = [];
    let settled = 0;
    const entries = Array.from(promises);

    if (entries.length === 0) {
      resolve([]);
      return;
    }

    entries.forEach((promise, index) => {
      Promise.resolve(promise).then(
        (value) => {
          results[index] = value; // maintain order
          settled++;
          if (settled === entries.length) {
            resolve(results);
          }
        },
        (reason) => {
          reject(reason); // fail fast on first rejection
        }
      );
    });
  });
}

// Usage
promiseAll([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3),
]).then(console.log); // [1, 2, 3]

promiseAll([
  Promise.resolve(1),
  Promise.reject("error"),
  Promise.resolve(3),
]).catch(console.log); // "error"

Polyfill: Promise.allSettled

ts
type SettledResult<T> =
  | { status: "fulfilled"; value: T }
  | { status: "rejected"; reason: any };

function promiseAllSettled<T>(
  promises: Array<T | Promise<T>>
): Promise<SettledResult<T>[]> {
  return new Promise((resolve) => {
    const results: SettledResult<T>[] = [];
    let settled = 0;
    const entries = Array.from(promises);

    if (entries.length === 0) {
      resolve([]);
      return;
    }

    entries.forEach((promise, index) => {
      Promise.resolve(promise).then(
        (value) => {
          results[index] = { status: "fulfilled", value };
          settled++;
          if (settled === entries.length) {
            resolve(results);
          }
        },
        (reason) => {
          results[index] = { status: "rejected", reason };
          settled++;
          if (settled === entries.length) {
            resolve(results);
          }
        }
      );
    });
  });
}

// Usage
promiseAllSettled([
  Promise.resolve("ok"),
  Promise.reject("fail"),
]).then(console.log);
// [
//   { status: "fulfilled", value: "ok" },
//   { status: "rejected", reason: "fail" }
// ]

async/await Error Handling Patterns

Pattern 1 — try/catch (most common):

ts
async function fetchUserBalance(userId: string) {
  try {
    const response = await fetch(`/api/users/${userId}/balance`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();
    return data.balance;
  } catch (error) {
    console.error("Failed to fetch balance:", error);
    throw error; // re-throw to let caller handle
  }
}

Pattern 2 — Wrapper returning [error, data] tuple (Go-style):

ts
async function to<T>(
  promise: Promise<T>
): Promise<[null, T] | [Error, null]> {
  try {
    const data = await promise;
    return [null, data];
  } catch (error) {
    return [error as Error, null];
  }
}

// Usage — no try/catch clutter
const [err, user] = await to(fetchUser(id));
if (err) {
  console.error(err);
  return;
}
console.log(user);

Pattern 3 — .catch() inline with await:

ts
const user = await fetchUser(id).catch(() => null);
if (!user) {
  // handle missing user
}

3. Currying

What Is Currying?

Currying transforms a function that takes multiple arguments into a sequence of functions that each take a single argument.

f(a, b, c)  →  f(a)(b)(c)

Generic Curry Function

ts
function curry(fn: Function) {
  return function curried(this: any, ...args: any[]): any {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...nextArgs: any[]) => {
      return curried.apply(this, [...args, ...nextArgs]);
    };
  };
}

// Usage
function add(a: number, b: number, c: number) {
  return a + b + c;
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3));    // 6
console.log(curriedAdd(1, 2)(3));    // 6
console.log(curriedAdd(1)(2, 3));    // 6
console.log(curriedAdd(1, 2, 3));    // 6

Infinite Currying

A common interview pattern: sum(1)(2)(3)...() returns the accumulated total when called with no arguments.

ts
function sum(a: number) {
  return function inner(b?: number): any {
    if (b === undefined) {
      return a; // termination — return accumulated value
    }
    return sum(a + b); // keep accumulating
  };
}

console.log(sum(1)(2)(3)());      // 6
console.log(sum(5)(10)(15)(20)()); // 50

Alternative using .valueOf() / .toString():

js
function sum(a) {
  function inner(b) {
    return sum(a + b);
  }
  inner.valueOf = () => a;
  inner.toString = () => `${a}`;
  return inner;
}

console.log(+sum(1)(2)(3));  // 6  (unary + triggers valueOf)

Practical Use: Specialized Functions

ts
const createLogger = curry(
  (level: string, prefix: string, message: string) => {
    console.log(`[${level}] ${prefix}: ${message}`);
  }
);

const errorLog = createLogger("ERROR");
const paymentErrorLog = errorLog("PaymentService");

paymentErrorLog("Transaction failed");
// [ERROR] PaymentService: Transaction failed

paymentErrorLog("Insufficient balance");
// [ERROR] PaymentService: Insufficient balance

4. Polyfills

Array.prototype.map

ts
Array.prototype.myMap = function <T, U>(
  this: T[],
  callback: (value: T, index: number, array: T[]) => U,
  thisArg?: any
): U[] {
  const result: U[] = [];
  for (let i = 0; i < this.length; i++) {
    // Skip holes in sparse arrays (same as native map)
    if (i in this) {
      result[i] = callback.call(thisArg, this[i], i, this);
    }
  }
  return result;
};

// How it works:
// 1. Iterate over each index of the array
// 2. Skip holes in sparse arrays (check with `i in this`)
// 3. Call the callback with (element, index, array), using thisArg as context
// 4. Push the return value into a new array
// 5. Return the new array (original is unchanged)

[1, 2, 3].myMap((x) => x * 2); // [2, 4, 6]

Array.prototype.filter

ts
Array.prototype.myFilter = function <T>(
  this: T[],
  callback: (value: T, index: number, array: T[]) => boolean,
  thisArg?: any
): T[] {
  const result: T[] = [];
  for (let i = 0; i < this.length; i++) {
    if (i in this) {
      if (callback.call(thisArg, this[i], i, this)) {
        result.push(this[i]);
      }
    }
  }
  return result;
};

// How it works:
// 1. Iterate over each index
// 2. Skip holes in sparse arrays
// 3. Call the callback — if it returns truthy, include the element
// 4. Return the new filtered array

[1, 2, 3, 4, 5].myFilter((x) => x > 3); // [4, 5]

Array.prototype.reduce

ts
Array.prototype.myReduce = function <T, U>(
  this: T[],
  callback: (accumulator: U, value: T, index: number, array: T[]) => U,
  initialValue?: U
): U {
  let accumulator: U;
  let startIndex: number;

  if (initialValue !== undefined) {
    accumulator = initialValue;
    startIndex = 0;
  } else {
    // If no initial value, use first element as accumulator
    if (this.length === 0) {
      throw new TypeError("Reduce of empty array with no initial value");
    }
    // Find the first non-hole index
    let found = false;
    let firstIndex = 0;
    for (let i = 0; i < this.length; i++) {
      if (i in this) {
        accumulator = this[i] as unknown as U;
        firstIndex = i;
        found = true;
        break;
      }
    }
    if (!found) {
      throw new TypeError("Reduce of empty array with no initial value");
    }
    startIndex = firstIndex + 1;
  }

  for (let i = startIndex; i < this.length; i++) {
    if (i in this) {
      accumulator = callback(accumulator!, this[i], i, this);
    }
  }

  return accumulator!;
};

// How it works:
// 1. Determine the starting accumulator (initialValue or first element)
// 2. If no initialValue and array is empty, throw TypeError
// 3. Iterate from startIndex, calling callback(accumulator, element, index, array)
// 4. Each iteration, the return value becomes the new accumulator
// 5. Return the final accumulator

[1, 2, 3, 4].myReduce((sum, val) => sum + val, 0); // 10

Function.prototype.bind

ts
Function.prototype.myBind = function (
  this: Function,
  context: any,
  ...boundArgs: any[]
) {
  const originalFn = this;

  // The bound function
  function boundFunction(this: any, ...callArgs: any[]) {
    // If called with `new`, `this` is an instance of boundFunction
    // In that case, ignore the provided context
    const isNewCall = this instanceof boundFunction;
    return originalFn.apply(
      isNewCall ? this : context,
      [...boundArgs, ...callArgs]
    );
  }

  // Maintain prototype chain so `new boundFunction()` works correctly
  if (originalFn.prototype) {
    boundFunction.prototype = Object.create(originalFn.prototype);
  }

  return boundFunction;
};

// How it works:
// 1. Store reference to the original function
// 2. Return a new function that, when called:
//    a. Checks if it was called with `new` (instanceof check)
//    b. If `new`, use the newly created `this`; otherwise use provided context
//    c. Merge bound args with call-time args
// 3. Copy the prototype so `new` works correctly

function greet(greeting: string, name: string) {
  return `${greeting}, ${name}!`;
}

const sayHello = greet.myBind(null, "Hello");
console.log(sayHello("Paytm")); // "Hello, Paytm!"

Debounce (with Immediate Option)

ts
function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number,
  immediate = false
): (...args: Parameters<T>) => void {
  let timerId: ReturnType<typeof setTimeout> | null = null;

  return function (this: any, ...args: Parameters<T>) {
    const callNow = immediate && timerId === null;

    if (timerId !== null) {
      clearTimeout(timerId);
    }

    timerId = setTimeout(() => {
      timerId = null;
      if (!immediate) {
        fn.apply(this, args);
      }
    }, delay);

    if (callNow) {
      fn.apply(this, args);
    }
  };
}

// How it works:
// 1. Each call resets the timer
// 2. The function only fires after `delay` ms of silence
// 3. With `immediate = true`, it fires on the LEADING edge (first call),
//    then ignores subsequent calls until silence
// 4. Preserves `this` context and arguments

// Use case: search input — don't hit API on every keystroke
const handleSearch = debounce((query: string) => {
  fetch(`/api/search?q=${query}`);
}, 300);

// With immediate — fire on first click, ignore rapid clicks
const handlePayment = debounce(() => {
  submitPayment();
}, 1000, true);

Throttle (Leading + Trailing)

ts
function throttle<T extends (...args: any[]) => any>(
  fn: T,
  interval: number,
  options: { leading?: boolean; trailing?: boolean } = {}
): (...args: Parameters<T>) => void {
  const { leading = true, trailing = true } = options;
  let lastCallTime: number | null = null;
  let timerId: ReturnType<typeof setTimeout> | null = null;
  let lastArgs: Parameters<T> | null = null;
  let lastThis: any = null;

  return function (this: any, ...args: Parameters<T>) {
    const now = Date.now();
    lastArgs = args;
    lastThis = this;

    // First call — if leading is false, pretend we just called it
    if (lastCallTime === null && !leading) {
      lastCallTime = now;
    }

    const elapsed = now - (lastCallTime ?? 0);
    const remaining = interval - elapsed;

    if (remaining <= 0) {
      // Enough time has passed — fire immediately
      if (timerId !== null) {
        clearTimeout(timerId);
        timerId = null;
      }
      lastCallTime = now;
      fn.apply(this, args);
      lastArgs = null;
      lastThis = null;
    } else if (timerId === null && trailing) {
      // Schedule a trailing call
      timerId = setTimeout(() => {
        lastCallTime = leading ? Date.now() : null;
        timerId = null;
        if (lastArgs) {
          fn.apply(lastThis, lastArgs);
          lastArgs = null;
          lastThis = null;
        }
      }, remaining);
    }
  };
}

// How it works:
// 1. On each call, check if enough time has passed since last invocation
// 2. If yes, execute immediately (leading edge)
// 3. If not, schedule a trailing call for when the interval expires
// 4. `leading: false` skips the immediate first call
// 5. `trailing: false` skips the delayed last call
// 6. Default: both leading and trailing are true

// Use case: scroll handler — fire at most once every 200ms
window.addEventListener(
  "scroll",
  throttle(() => {
    updateScrollPosition();
  }, 200)
);

5. The this Keyword

Rules of this (in Order of Precedence)

PriorityRuleExamplethis is
1 (highest)new bindingnew Foo()The newly created object
2Explicit bindingfn.call(obj) / fn.apply(obj) / fn.bind(obj)obj
3Implicit bindingobj.fn()obj (the object before the dot)
4 (lowest)Default bindingfn()globalThis (sloppy mode) or undefined (strict mode)
SpecialArrow function() => {}Inherits this from enclosing lexical scope

Code Examples for Each Rule

1. new Binding:

js
function User(name) {
  this.name = name;
}

const user = new User("Paytm");
console.log(user.name); // "Paytm"
// `this` inside User refers to the newly created object

2. Explicit Binding (call, apply, bind):

js
function greet() {
  console.log(`Hello, I'm ${this.name}`);
}

const user = { name: "Aayush" };

greet.call(user);       // "Hello, I'm Aayush"
greet.apply(user);      // "Hello, I'm Aayush"

const bound = greet.bind(user);
bound();                // "Hello, I'm Aayush"

3. Implicit Binding (method call):

js
const wallet = {
  balance: 1000,
  getBalance() {
    return this.balance; // `this` is `wallet`
  },
};

console.log(wallet.getBalance()); // 1000

// GOTCHA: losing implicit binding
const fn = wallet.getBalance;
console.log(fn()); // undefined — `this` is now global/undefined

4. Default Binding:

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

showThis(); // globalThis (browser: window) in sloppy mode
            // undefined in strict mode

Arrow Functions (Lexical this)

Arrow functions do NOT have their own this. They capture this from the enclosing scope at the time they are defined.

ts
const payment = {
  amount: 500,
  process() {
    // Regular function — `this` is `payment`
    console.log("Processing:", this.amount);

    setTimeout(() => {
      // Arrow function — `this` is inherited from `process()`
      console.log("Confirmed:", this.amount); // 500
    }, 1000);

    setTimeout(function () {
      // Regular function — `this` is globalThis/undefined
      console.log("Lost:", this.amount); // undefined
    }, 1000);
  },
};

payment.process();

Common Interview Questions

Q: What is the output?

js
const obj = {
  name: "Paytm",
  getName: () => {
    return this.name;
  },
};

console.log(obj.getName());

A: undefined. The arrow function captures this from the enclosing scope (module/global), not from obj. Arrow functions should not be used as methods.


Q: What is the output?

js
const obj = {
  name: "Paytm",
  greet() {
    return function () {
      return this.name;
    };
  },
};

console.log(obj.greet()()); // ?

A: undefined (strict mode) or "" / window.name (sloppy mode). The inner function is called without a receiver, so default binding applies. Fix: use an arrow function for the inner function, or .bind(this).


Q: What is the output?

js
function Foo() {
  this.value = 42;
  return { value: 100 };
}

const foo = new Foo();
console.log(foo.value); // ?

A: 100. When a constructor explicitly returns an object, new uses that object instead of the one it created. If the return value is a primitive, it is ignored.


6. Prototypal Inheritance

Prototype Chain

Every JavaScript object has an internal [[Prototype]] link. When you access a property on an object and it is not found, the engine walks up the prototype chain until it finds the property or reaches null.

myObj → MyConstructor.prototype → Object.prototype → null
js
const animal = {
  type: "Animal",
  speak() {
    return `${this.name} makes a sound`;
  },
};

const dog = Object.create(animal); // dog's [[Prototype]] = animal
dog.name = "Rex";

console.log(dog.speak());  // "Rex makes a sound" — found on animal
console.log(dog.type);     // "Animal" — found on animal
console.log(dog.hasOwnProperty("name"));  // true
console.log(dog.hasOwnProperty("speak")); // false — it's on the prototype

__proto__ vs prototype vs Object.getPrototypeOf()

TermWhat it is
obj.__proto__Accessor to get/set the [[Prototype]] of an object. Deprecated but widely supported.
Constructor.prototypeThe object that becomes the [[Prototype]] of instances created with new Constructor().
Object.getPrototypeOf(obj)The standard way to read an object's [[Prototype]]. Use this instead of __proto__.
js
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function () {
  return `Hi, I'm ${this.name}`;
};

const p = new Person("Aayush");

console.log(p.__proto__ === Person.prototype);             // true
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
console.log(Person.prototype.constructor === Person);       // true

How new Works (4 Steps)

When you call new Constructor(args), the engine does the following:

js
function myNew(Constructor, ...args) {
  // Step 1: Create a new empty object
  const obj = {};

  // Step 2: Set its [[Prototype]] to Constructor.prototype
  Object.setPrototypeOf(obj, Constructor.prototype);

  // Step 3: Call the constructor with `this` = the new object
  const result = Constructor.apply(obj, args);

  // Step 4: If the constructor returns an object, use that;
  //         otherwise return the new object
  return result instanceof Object ? result : obj;
}

// Verification
function User(name) {
  this.name = name;
}

const user = myNew(User, "Paytm");
console.log(user.name);                // "Paytm"
console.log(user instanceof User);     // true

ES6 class as Syntactic Sugar

ES6 classes are syntactic sugar over the prototype-based pattern. Under the hood, they work exactly the same way.

ts
// ES6 Class
class Transaction {
  id: string;
  amount: number;

  constructor(id: string, amount: number) {
    this.id = id;
    this.amount = amount;
  }

  describe() {
    return `Transaction ${this.id}: ₹${this.amount}`;
  }
}

class Refund extends Transaction {
  reason: string;

  constructor(id: string, amount: number, reason: string) {
    super(id, amount);
    this.reason = reason;
  }

  describe() {
    return `${super.describe()} [Refund: ${this.reason}]`;
  }
}

const refund = new Refund("TXN-001", 500, "Duplicate charge");
console.log(refund.describe());
// "Transaction TXN-001: ₹500 [Refund: Duplicate charge]"

Equivalent prototype-based code:

js
function Transaction(id, amount) {
  this.id = id;
  this.amount = amount;
}

Transaction.prototype.describe = function () {
  return `Transaction ${this.id}: ₹${this.amount}`;
};

function Refund(id, amount, reason) {
  Transaction.call(this, id, amount); // super()
  this.reason = reason;
}

Refund.prototype = Object.create(Transaction.prototype);
Refund.prototype.constructor = Refund;

Refund.prototype.describe = function () {
  return `${Transaction.prototype.describe.call(this)} [Refund: ${this.reason}]`;
};

Object.create() for Inheritance

Object.create(proto) creates a new object with proto as its [[Prototype]].

js
const base = {
  init(name) {
    this.name = name;
    return this;
  },
  getName() {
    return this.name;
  },
};

const child = Object.create(base);
child.init("Paytm");
console.log(child.getName()); // "Paytm"

// Useful for creating objects without constructors
// Also used internally by `new` (step 2)

7. Hoisting & Temporal Dead Zone

var Hoisting

var declarations are hoisted to the top of their function scope, but the initialization stays in place. The variable exists from the start of the scope with the value undefined.

js
console.log(x); // undefined (not ReferenceError!)
var x = 10;
console.log(x); // 10

// The engine sees it as:
// var x;            ← declaration hoisted
// console.log(x);   ← undefined
// x = 10;           ← assignment stays
// console.log(x);   ← 10

Function Declaration vs Function Expression Hoisting

Function declarations are fully hoisted (both name and body):

js
sayHello(); // "Hello!" — works because the entire function is hoisted

function sayHello() {
  console.log("Hello!");
}

Function expressions are NOT fully hoisted (only the variable declaration is):

js
sayHello(); // TypeError: sayHello is not a function

var sayHello = function () {
  console.log("Hello!");
};

// With `var`, sayHello is hoisted as undefined
// So calling it throws TypeError (not ReferenceError)
js
sayHello(); // ReferenceError: Cannot access 'sayHello' before initialization

const sayHello = function () {
  console.log("Hello!");
};

// With `const`, sayHello is in the TDZ
// So calling it throws ReferenceError

let/const and the Temporal Dead Zone

let and const declarations ARE hoisted, but they are placed in the Temporal Dead Zone (TDZ) from the start of the block until the declaration is encountered. Accessing a variable in the TDZ throws a ReferenceError.

js
{
  // TDZ for `x` starts here
  console.log(x); // ReferenceError: Cannot access 'x' before initialization
  let x = 10;     // TDZ ends here
  console.log(x); // 10
}
js
// This proves `let` IS hoisted (just in TDZ):
let x = "outer";
{
  // If `let x` below were NOT hoisted, this would print "outer"
  console.log(x); // ReferenceError — proves the inner `x` is hoisted
  let x = "inner";
}

Why TDZ Exists

  1. Prevents use before declaration — catches bugs where you accidentally reference a variable before it is ready.
  2. Makes const semantics possibleconst must be initialized at declaration. Without TDZ, it would be undefined before the declaration line, violating the "constant" guarantee.
  3. Encourages cleaner code — forces developers to declare variables before use.

Interview Trick Questions

Q1: What is the output?

js
var a = 1;
function foo() {
  console.log(a);
  var a = 2;
}
foo();

A: undefined. The local var a is hoisted within foo, shadowing the outer a. At the time of console.log, the local a exists but has not been assigned yet.


Q2: What is the output?

js
console.log(typeof undeclaredVar);
console.log(typeof letVar);
let letVar = 10;

A: First line prints "undefined" (typeof on undeclared variables does not throw). Second line throws ReferenceError because letVar is in the TDZ. The TDZ even affects typeof.


Q3: What is the output?

js
function test() {
  console.log(a); // ?
  console.log(b); // ?
  console.log(c); // ?

  var a = 1;
  let b = 2;
  const c = 3;
}
test();

A: undefined for a (var hoisting), then ReferenceError for b (TDZ). The line for c is never reached.


Q4: What is the output?

js
foo();
bar();

function foo() {
  console.log("foo");
}

var bar = function () {
  console.log("bar");
};

A: "foo" is printed (function declaration fully hoisted). Then TypeError: bar is not a function (only the var bar declaration is hoisted as undefined, not the function assignment).

Frontend interview preparation reference.