JavaScript Fundamentals
Quick Reference
Scan this table in 5 minutes before your interview.
| Topic | Key Points | Common Interview Question |
|---|---|---|
| Closures | Function + 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? |
| Promises | 3 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. |
| Currying | Transform 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. |
| Polyfills | Know map, filter, reduce, bind, debounce, throttle internals. Respect thisArg, handle edge cases. | Implement Array.prototype.reduce from scratch. |
this Keyword | Precedence: 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 Inheritance | Every 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 & TDZ | var 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.
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 privacyScope Chain
When a variable is referenced, the engine walks up the scope chain:
Current Function Scope → Outer Function Scope → ... → Global Scopeconst 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:
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):
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:
for (var i = 0; i < 3; i++) {
(function (j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Output: 0, 1, 2Practical Uses
Data Privacy (Module Pattern):
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:
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)); // 15Partial Application:
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.
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 calledPromise Combinators
Promise.all
All must resolve. Fails fast on the first rejection.
const results = await Promise.all([
fetch("/api/user"),
fetch("/api/transactions"),
fetch("/api/balance"),
]);
// If any one rejects, the whole thing rejects immediatelyPromise.allSettled
Waits for all promises to settle. Never short-circuits. Returns an array of { status, value/reason }.
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.
const result = await Promise.race([
fetch("/api/primary"),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), 5000)
),
]);
// Useful for implementing request timeoutsPromise.any
First promise to fulfill wins. Ignores rejections unless all reject (then throws AggregateError).
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 CDNsPolyfill: Promise.all
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
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):
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):
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:
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
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)); // 6Infinite Currying
A common interview pattern: sum(1)(2)(3)...() returns the accumulated total when called with no arguments.
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)()); // 50Alternative using .valueOf() / .toString():
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
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 balance4. Polyfills
Array.prototype.map
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
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
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); // 10Function.prototype.bind
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)
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)
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)
| Priority | Rule | Example | this is |
|---|---|---|---|
| 1 (highest) | new binding | new Foo() | The newly created object |
| 2 | Explicit binding | fn.call(obj) / fn.apply(obj) / fn.bind(obj) | obj |
| 3 | Implicit binding | obj.fn() | obj (the object before the dot) |
| 4 (lowest) | Default binding | fn() | globalThis (sloppy mode) or undefined (strict mode) |
| Special | Arrow function | () => {} | Inherits this from enclosing lexical scope |
Code Examples for Each Rule
1. new Binding:
function User(name) {
this.name = name;
}
const user = new User("Paytm");
console.log(user.name); // "Paytm"
// `this` inside User refers to the newly created object2. Explicit Binding (call, apply, bind):
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):
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/undefined4. Default Binding:
function showThis() {
console.log(this);
}
showThis(); // globalThis (browser: window) in sloppy mode
// undefined in strict modeArrow Functions (Lexical this)
Arrow functions do NOT have their own this. They capture this from the enclosing scope at the time they are defined.
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?
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?
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?
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 → nullconst 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()
| Term | What it is |
|---|---|
obj.__proto__ | Accessor to get/set the [[Prototype]] of an object. Deprecated but widely supported. |
Constructor.prototype | The 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__. |
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); // trueHow new Works (4 Steps)
When you call new Constructor(args), the engine does the following:
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); // trueES6 class as Syntactic Sugar
ES6 classes are syntactic sugar over the prototype-based pattern. Under the hood, they work exactly the same way.
// 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:
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]].
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.
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); ← 10Function Declaration vs Function Expression Hoisting
Function declarations are fully hoisted (both name and body):
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):
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)sayHello(); // ReferenceError: Cannot access 'sayHello' before initialization
const sayHello = function () {
console.log("Hello!");
};
// With `const`, sayHello is in the TDZ
// So calling it throws ReferenceErrorlet/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.
{
// TDZ for `x` starts here
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10; // TDZ ends here
console.log(x); // 10
}// 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
- Prevents use before declaration — catches bugs where you accidentally reference a variable before it is ready.
- Makes
constsemantics possible —constmust be initialized at declaration. Without TDZ, it would beundefinedbefore the declaration line, violating the "constant" guarantee. - Encourages cleaner code — forces developers to declare variables before use.
Interview Trick Questions
Q1: What is the output?
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?
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?
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?
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).