How to use this guide: Read each question, try to answer it in your head first, then read the answer. This active recall method is the fastest way to prepare for technical interviews.
Fundamentals (Q1–Q8)
Section 1var, let, and const?var is function-scoped, can be redeclared, and is hoisted with an undefined value. let is block-scoped, cannot be redeclared in the same scope, and is hoisted but not initialized (Temporal Dead Zone). const is like let but the binding cannot be reassigned — though objects and arrays declared with const can still be mutated. In modern JavaScript, you should almost always use const by default and let when you need to reassign. Avoid var.
Hoisting is JavaScript's behavior of moving variable and function declarations to the top of their scope during the compilation phase — before code executes. Function declarations are fully hoisted (you can call them before they appear in the code). var is hoisted but initialized to undefined. let and const are hoisted but not initialized — accessing them before declaration throws a ReferenceError (this is called the Temporal Dead Zone).
== and ===?== is the loose equality operator — it performs type coercion before comparing. For example, 0 == false is true because false gets converted to 0. === is the strict equality operator — it compares both value and type without coercion. 0 === false is false because they are different types. Always use === in production code to avoid unexpected behavior.
JavaScript has 8 data types: 7 primitives — string, number, bigint, boolean, undefined, null, symbol — and 1 non-primitive: object (which includes arrays, functions, and objects). Primitives are immutable and stored by value. Objects are stored by reference. A common gotcha: typeof null returns "object" — this is a known bug in JavaScript from its early days.
null and undefined?undefined means a variable has been declared but not assigned a value. JavaScript sets it automatically. null is an intentional assignment that means "no value" — you set it explicitly. Think of undefined as "not yet set" and null as "deliberately empty." They are loosely equal (null == undefined is true) but not strictly equal (null === undefined is false).
Type coercion is the automatic or implicit conversion of values from one data type to another. For example, "5" + 3 gives "53" because 3 gets coerced to a string. But "5" - 3 gives 2 because the string gets coerced to a number. The + operator favors strings; arithmetic operators favor numbers. Understanding coercion is key to avoiding bugs in JavaScript.
typeof and instanceof?typeof returns a string indicating the type of a value: "string", "number", "boolean", "object", "function", "undefined", or "symbol". It works for primitives. instanceof checks whether an object was created by a specific constructor. For example, [] instanceof Array returns true. Use typeof for primitives and instanceof for objects/classes.
Primitive values (strings, numbers, booleans) are immutable — you cannot change them in place. When you "change" a string, you actually create a new string. Objects and arrays are mutable — you can change their contents without creating a new object. This is why passing an object to a function and modifying it inside the function affects the original object (passed by reference), while primitives are unaffected (passed by value).
Functions & Scope (Q9–Q14)
Section 2A closure is a function that remembers the variables from its outer scope even after the outer function has returned. In JavaScript, every function creates a closure over its enclosing scope. A classic example is a counter factory: the inner function keeps access to the count variable even after the outer function finishes. Closures are used for data encapsulation, memoization, and creating private state.
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2 — count is rememberedA function declaration (function foo() {}) is fully hoisted — you can call it before it appears in the code. A function expression (const foo = function() {}) is not fully hoisted — the variable is hoisted but the function is not assigned until that line runs. Arrow functions (const foo = () => {}) are also function expressions. In practice, this means you must define function expressions before calling them.
this keyword and how does it behave?this refers to the object that is currently executing the function. Its value depends on how the function is called, not where it is defined. In a method, this is the object the method belongs to. In a regular function called alone, this is undefined (strict mode) or the global object. In arrow functions, this is inherited from the enclosing scope — arrow functions do not have their own this.
call, apply, and bind?All three are methods to explicitly set the value of this. call(obj, arg1, arg2) calls the function immediately with this set to obj and arguments passed individually. apply(obj, [args]) is the same but arguments are passed as an array. bind(obj) does not call the function immediately — it returns a new function with this permanently bound to obj.
Arrow functions: (1) do not have their own this — they inherit this from the surrounding context; (2) cannot be used as constructors; (3) do not have arguments object; (4) cannot be used as generator functions. Regular functions have all these capabilities. Arrow functions are ideal for callbacks and short utility functions. Avoid arrow functions for object methods or event handlers where you need dynamic this.
Scope determines where variables are accessible in your code. JavaScript has 4 types: Global scope — variables declared outside any function, accessible everywhere. Function scope — variables declared with var inside a function. Block scope — variables declared with let or const inside {} blocks. Module scope — variables in ES6 modules are scoped to that module. JavaScript uses lexical scoping — a function's scope is determined by where it is written, not where it is called.
Async JavaScript (Q15–Q20)
Section 3JavaScript is single-threaded — it executes one thing at a time. The event loop manages asynchronous operations. It works like this: the call stack executes synchronous code. When async operations (timers, fetch) complete, their callbacks go to a task queue. The event loop continuously checks: if the call stack is empty, it takes the next item from the queue and pushes it to the stack. Promises use a microtask queue, which has higher priority than the regular task queue.
A Promise is an object that represents the eventual result of an asynchronous operation. It has 3 states: pending (initial), fulfilled (resolved with a value), or rejected (failed with an error). You handle results with .then() and errors with .catch(). Promises solve "callback hell" by allowing you to chain async operations cleanly instead of nesting callbacks.
async/await and how does it relate to Promises?async/await is syntactic sugar built on top of Promises. An async function always returns a Promise. Inside an async function, await pauses execution until the Promise resolves, allowing you to write async code that reads like synchronous code. Under the hood, await is just .then(). Use try/catch for error handling instead of .catch().
Promise.all() and when would you use it?Promise.all() takes an array of Promises and returns a new Promise that resolves when all input Promises resolve, or rejects as soon as any one rejects. Use it when you need to run multiple async operations in parallel and wait for all of them. Example: fetching user data, posts, and notifications simultaneously instead of one by one. Promise.allSettled() is similar but waits for all Promises regardless of success or failure.
The Fetch API is a modern, Promise-based way to make HTTP requests in the browser — replacing the older XMLHttpRequest. fetch(url) returns a Promise that resolves to a Response object. You then call .json() on the response (also a Promise) to parse the JSON body. Always check response.ok because fetch only rejects on network errors — a 404 or 500 response will still resolve.
const res = await fetch('https://api.example.com/data');
if (!res.ok) throw new Error('Request failed');
const data = await res.json();A callback is a function passed as an argument to another function to be executed later — the foundation of async JavaScript. "Callback hell" (also called "pyramid of doom") happens when you nest multiple callbacks inside each other, creating deeply indented, hard-to-read code. Promises and async/await were designed specifically to solve this problem by allowing async code to be written in a flat, readable style.
ES6+ Features (Q21–Q25)
Section 4Destructuring is a syntax that lets you unpack values from arrays or properties from objects into distinct variables. Array destructuring: const [a, b] = [1, 2]. Object destructuring: const { name, age } = user. You can also rename variables (const { name: userName } = user) and set defaults (const { role = 'user' } = options). It's commonly used in function parameters to extract specific properties from objects.
Both use the ... syntax but do opposite things. The spread operator expands an iterable (array, object) into individual elements: const merged = [...arr1, ...arr2]. The rest parameter collects multiple arguments into a single array: function sum(...nums) { return nums.reduce((a,b) => a+b, 0); }. Spread is used at call sites; rest is used in function definitions.
Template literals use backticks (`) instead of quotes and allow embedded expressions with ${expression} syntax and multiline strings without escape characters. They are much cleaner than string concatenation. Example: `Hello, ${user.name}! You have ${count} messages.`. They also support tagged templates — functions that process the template literal — used by libraries like styled-components.
ES6 modules allow you to split code across files. export makes values available to other files. import brings them in. Named exports: export const add = (a, b) => a + b — imported with the same name: import { add } from './math.js'. Default export: one per file, imported without braces: import Calculator from './Calculator.js'. Modules are always in strict mode and are scoped — variables don't leak to global scope.
?.) and nullish coalescing (??)?Optional chaining (?.) lets you safely access deeply nested properties without throwing if an intermediate value is null or undefined: user?.address?.city returns undefined instead of crashing. Nullish coalescing (??) returns the right-hand value only if the left side is null or undefined: const name = user.name ?? 'Anonymous'. Unlike ||, it does not treat 0 or '' as falsy.
Advanced Concepts (Q26–Q30)
Section 5In JavaScript, objects inherit from other objects via a prototype chain. Every object has an internal [[Prototype]] reference (accessible via Object.getPrototypeOf()). When you access a property, JavaScript looks at the object first, then walks up the prototype chain until it finds it or reaches null. ES6 classes are syntactic sugar over this prototype system — they don't introduce a new inheritance model, they just make the existing one easier to use.
When you click an element, the event travels in two phases. Capturing phase: the event travels down from the document root to the target element. Bubbling phase: the event travels back up from the target to the root. By default, event listeners fire in the bubbling phase. You can use capturing with addEventListener('click', fn, true). event.stopPropagation() stops the event from traveling further. Event delegation uses bubbling — you add one listener to a parent instead of many listeners to children.
A shallow copy copies only the top-level properties of an object. Nested objects are still shared by reference. Object.assign({}, obj) and the spread operator {...obj} create shallow copies. A deep copy recursively copies all nested objects so the copy is completely independent. JSON.parse(JSON.stringify(obj)) creates a deep copy but loses functions and special values. structuredClone(obj) is the modern, built-in way to deep clone objects.
A higher-order function (HOF) is a function that either takes one or more functions as arguments or returns a function. Array.prototype.map(), filter(), and reduce() are the most common HOFs. They allow you to write declarative, composable code instead of imperative loops. Closures make HOFs possible — the returned function carries its surrounding context. HOFs are fundamental to functional programming patterns in JavaScript.
Both are performance optimization techniques to control how often a function runs. Debouncing delays function execution until a specified time has passed since the last call. It's used for search inputs — wait until the user stops typing before fetching results. Throttling limits how often a function runs over time — at most once per N milliseconds. It's used for scroll and resize events where you want regular updates but not on every pixel change. Libraries like Lodash provide both as _.debounce() and _.throttle().
Interview tip: For every question above, practice saying your answer out loud. Technical interviews test verbal explanation just as much as code knowledge. Being able to explain a closure in plain English is what separates candidates who get offers.