Junior Question: What’s the difference between `null` and `undefined`?
Expected Answer
- `undefined` means a value was not provided / missing.
- `null` is an explicit “no value” assignment.
- `typeof undefined` → `"undefined"`, `typeof null` → `"object"`.
Top JavaScript interview questions and answers for freshers and experienced developers.
Top JavaScript interview questions and answers for freshers and experienced developers.
const person = { name: 'Ava' };
person.name = 'Noah'; // ok (mutation)
person = { name: 'Mia' }; // error (reassign)Declarations are moved to the top of their scope before code runs. For variables declared with `var`, initialization becomes `undefined`. `let`/`const` are in the temporal dead zone until initialized.
A closure is a function that retains access to variables from its outer scope even after the outer function has finished.
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const c = makeCounter();
console.log(c()); // 1
console.log(c()); // 2console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve()
.then(() => console.log('C'))
.then(() => console.log('D'));
console.log('E');A, E, C, D, B
var x = 10;
function test() {
console.log(x);
let x = 20;
}
test();It throws a ReferenceError due to the temporal dead zone for the inner `let x`.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}3, 3, 3 (because `var` is function-scoped and `i` ends as 3).
const p = Promise.resolve('X');
p.then(v => {
console.log(v);
return 'Y';
}).then(v => console.log(v));
console.log('Z');Z, X, Y
How would you investigate?
What do you do first?
How would you narrow down the cause?
What’s your debugging approach?
How would you respond?
How do you investigate?
What metrics and tools do you use?
How would you debug?
Walk through:
Explain the debugging and mitigation plan.
How do you detect and fix it?
What’s your response workflow?
How do you systematically debug?
What causes this and how do you fix it?
How do you investigate?
What questions do you ask and what do you check?
How do you diagnose and improve it?
What’s your approach?
console.log(typeof null);
console.log(typeof undefined);
console.log(typeof []);object, undefined, object
console.log(0 == false);
console.log(0 === false);
console.log('0' == 0);
console.log('0' === 0);true, false, true, false
let a = 1;
if (true) {
let a = 2;
console.log(a);
}
console.log(a);2 then 1
console.log('start');
queueMicrotask(() => console.log('micro 1'));
Promise.resolve()
.then(() => console.log('promise then'))
.then(() => console.log('promise then 2'));
setTimeout(() => console.log('timeout'), 0);
console.log('end');start, end, micro 1, promise then, promise then 2, timeout
async function run() {
console.log(1);
await Promise.resolve();
console.log(2);
}
run();
console.log(3);1, 3, 2
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 10);
}
console.log('done');done then 3, 3, 3
console.log(a);
let a = 1;ReferenceError (accessing `a` before initialization)
const obj = {
x: 10,
f: function () {
setTimeout(function () {
console.log(this.x);
}, 0);
}
};
obj.f();undefined (or error depending on environment), because `this` inside the normal function isn’t `obj`.
const obj = {
x: 10,
f: function () {
setTimeout(function () {
console.log(this.x);
}, 0);
}
};const obj = {
x: 10,
f: function () {
setTimeout(() => {
console.log(this.x);
}, 0);
}
};{ capture: true }.function safeAsync(fn) {
return async (...args) => {
try {
return await fn(...args);
} catch (err) {
// log, report, or return a fallback
console.error('safeAsync caught:', err);
throw err; // or return { ok:false, err }
}
};
}JavaScript variables can hold two categories of values:
// Primitive value copy
let a = 10;
let b = a;
b = 20; // 'a' is still 10
// Reference value copy
let obj1 = { name: 'Alice' };
let obj2 = obj1;
obj2.name = 'Bob'; // obj1.name is now 'Bob' because both share the same reference!
Interviewer Note: 💡 Check if the candidate knows that stack accesses are faster than heap lookups, which require resolving pointer references.
Copying objects can be done at different depths:
{...obj}, Object.assign().structuredClone() (modern ES), JSON.parse(JSON.stringify(obj)) (has limitations).
const original = { name: 'Alice', details: { age: 30 } };
// Shallow Copy
const shallow = { ...original };
shallow.details.age = 40; // original.details.age is now 40!
// Deep Copy
const deep = structuredClone(original);
deep.details.age = 50; // original.details.age is still 40!
Interviewer Note: 💡 Ask about limitations of JSON serialization (loses functions, regexes, undefined values, and symbols).
JavaScript modules allow code segregation. The two main systems are:
import and export statements. It is statically analyzed, resolving dependencies at compile time, which enables tree-shaking. Runs asynchronously.require() and module.exports. It is dynamic, loading scripts synchronously at runtime.Interviewer Note: 💡 Look for candidate's knowledge that ESM is native to browsers whereas CommonJS requires bundlers to run in browsers.
Optional Chaining and Nullish Coalescing simplify nested property accesses and fallback assignments:
?.): Safely reads nested values. If a parent reference is null/undefined, it returns undefined instead of throwing a TypeError.??): Returns the fallback value only when the left expression is nullish (null or undefined).||): The OR operator returns the fallback for any falsy value (such as 0, "", or false), whereas ?? respects these values.
const config = { count: 0, text: '' };
const count1 = config.count || 10; // returns 10 because 0 is falsy
const count2 = config.count ?? 10; // returns 0 because 0 is not nullish
Interviewer Note: 💡 Candidates should show clear code predictability benefits when using Nullish Coalescing instead of standard logical OR.
Event Delegation is a pattern where you attach a single event listener to a parent element rather than attaching listeners to individual children. Due to Event Bubbling, events fired on child elements bubble up to parent containers where they can be intercepted.
// Single listener on parent ul container
document.getElementById('myList').addEventListener('click', (event) => {
if (event.target.tagName === 'LI') {
console.log('Item clicked:', event.target.innerText);
}
});
Interviewer Note: 💡 A core DOM API concept. Make sure they mention event.target and event bubbling.
ES6 introduced Map and Set structures to resolve limitations in Objects and Arrays:
size properties out of the box.set.has().Interviewer Note: 💡 Good candidates discuss complexity: checking element presence in a Set is O(1), whereas in an Array it is O(N).
These methods are used to set the this context of a function call explicitly:
fn.call(context, arg1, arg2).fn.apply(context, [arg1, arg2]).this permanently bound, allowing it to be called later.
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const user = { name: 'Alice' };
// Immediate execution
greet.call(user, 'Hello', '!'); // 'Hello, Alice!'
greet.apply(user, ['Hi', '.']); // 'Hi, Alice.'
// Deferred execution
const bound = greet.bind(user);
bound('Welcome', '?'); // 'Welcome, Alice?'
Interviewer Note: 💡 Check if the candidate knows that arrow functions cannot have their 'this' context overridden by bind/call/apply.
These concepts define how variables are resolved and how functions execute:
this binding. It has two phases: creation (hoisting, scope building) and execution.Interviewer Note: 💡 Candidates should trace how nested function executions create stacked context layers.
WeakMap and WeakSet differ in how they hold references to keys and values, which prevents memory leaks:
let user = { name: 'John' };
const wm = new WeakMap();
wm.set(user, 'profile info');
user = null; // The object is now eligible for Garbage Collection!
// In a standard Map, the object would stay in memory until the Map is cleared.
Interviewer Note: 💡 Ask about limitations: WeakMaps cannot be iterated over, and you cannot read their size properties.
AbortController is a built-in browser API that allows you to abort one or more Web requests dynamically.
new AbortController() to get an object containing an abort() method and a signal property.signal reference as an option to your fetch() request.controller.abort() cancels the request, causing the fetch promise to reject with an AbortError.
const controller = new AbortController();
const { signal } = controller;
fetch('/api/heavy-data', { signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch request was successfully aborted.');
}
});
// Abort the request after 2 seconds
setTimeout(() => controller.abort(), 2000);
Interviewer Note: 💡 Important for performance: ask them how to use it to prevent race conditions in search autocomplete inputs.
Generators and Iterators enable custom traversal of data structures:
next() method that returns `{ value, done }`.function* that returns a generator iterator. It uses the yield keyword to pause execution and emit values lazily.
function* numberGenerator() {
let i = 0;
while (true) {
yield i++;
}
}
const gen = numberGenerator();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
Interviewer Note: 💡 A mid-level JS check. Verify they know how generators yield execution control back to caller scopes.
A Symbol is a unique primitive value introduced in ES6.
Symbol() call returns a unique value, preventing key collisions in objects.Object.keys() or for...in loops. They are only readable using Object.getOwnPropertySymbols().Symbol.iterator or Symbol.toStringTag to define core object protocols.
const privateKey = Symbol('id');
const user = {
[privateKey]: 'secret-101',
name: 'Alice'
};
console.log(Object.keys(user)); // ['name']
console.log(user[privateKey]); // 'secret-101'
Interviewer Note: 💡 Ask: 'Can Symbols be parsed from JSON?' (Answer: No, JSON.stringify skips Symbol properties).
The V8 Engine uses a generational garbage collector to reclaim unused heap memory. Memory is split into two generations:
Interviewer Note: 💡 Candidates should understand the 'Weak Generational Hypothesis': most objects die young, which is why V8 separates memory spaces.
Both limit execution rate, but behave differently:
function debounce(fn, delay) {
let timerId = null;
return function (...args) {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
Interviewer Note: 💡 Check if the candidate handles 'this' context and parameters inside the debounced returned function.
Currying transforms a function that takes multiple arguments into a chain of nested functions that each take a single argument: fn(a, b, c) becomes fn(a)(b)(c).
This enables partial function application, which helps build re-usable configuration functions.
// Currying helper
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
};
}
// Usage
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
Interviewer Note: 💡 Candidates should write the curry helper using recursion, comparing argument lengths to determine when to execute the target function.
The Critical Rendering Path (CRP) is the sequence of steps the browser takes to convert HTML, CSS, and JS into visible pixels on the screen:
display: none).Interviewer Note: 💡 Ask the candidate how script tags impact DOM parsing (blocking unless async or defer attributes are set).
Web Workers allow running scripts in background threads, isolated from the browser's main thread.
postMessage() and onmessage.
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ number: 40 });
worker.onmessage = (e) => {
console.log('Result from worker:', e.data.result);
};
// worker.js
self.onmessage = (e) => {
const result = fibonacci(e.data.number);
self.postMessage({ result });
};
Interviewer Note: 💡 Verify they understand that Web Workers use message serialization (structured clone algorithm) to share data across threads.
These methods restrict modifications to objects at different levels:
Interviewer Note: 💡 Check if the candidate knows these restrictions only throw errors in Strict Mode; in non-strict mode, they fail silently.
The Proxy and Reflect APIs work together to intercept and customize operations on objects:
get, set).
const target = { count: 0 };
const proxy = new Proxy(target, {
get(target, prop, receiver) {
console.log(`Accessing property: ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`Setting property: ${prop} = ${value}`);
// Trigger side-effects here (e.g. UI re-renders)
return Reflect.set(target, prop, value, receiver);
}
});
proxy.count = 5; // logs 'Setting property: count = 5'
Interviewer Note: 💡 Look for the candidate's understanding of Receiver arguments: they ensure correct inheritance when calling getters/setters on inherited objects.
Interviewer Note: 💡 This is an advanced compiler query. Ask the candidate why adding properties dynamically at runtime (causing polymorphic shapes) slows down execution.
The Event Loop processes asynchronous tasks sequentially:
setTimeout, setInterval, postMessage, and standard DOM events. They run one at a time.Promise.then(), queueMicrotask, and MutationObserver. The microtask queue is **flushed completely** after the current execution context, before returning control to the Event Loop.
1. Execute a macrotask (e.g., initial script).
2. Run all microtasks until the queue is empty.
3. Check if rendering is needed and repaint if necessary.
4. Fetch the next macrotask from the queue.
Interviewer Note: 💡 Verify they understand that an infinite loop of microtasks (e.g., recursive Promise loops) starves the event loop, freezing the browser and blocking UI rendering.
Build a Promise class implementing states (PENDING, FULFILLED, REJECTED) and managing handlers dynamically. It must execute handlers asynchronously (using microtasks) to comply with standard specs.
class MyPromise {
constructor(executor) {
this.state = 'PENDING';
this.value = null;
this.handlers = [];
const resolve = (val) => {
if (this.state !== 'PENDING') return;
this.state = 'FULFILLED';
this.value = val;
this.handlers.forEach(h => h.onFulfilled(val));
};
const reject = (err) => {
if (this.state !== 'PENDING') return;
this.state = 'REJECTED';
this.value = err;
this.handlers.forEach(h => h.onRejected(err));
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
}
Interviewer Note: 💡 Look for correct handler queuing when then() is called after the promise has already resolved.
Modern ECMAScript specifications introduce features that simplify common patterns:
map, filter, take, drop) directly to JS generators and iterators.Date object.
// Promise.withResolvers
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => resolve('done'), 1000);
// Object.groupBy
const list = [{ role: 'admin' }, { role: 'user' }];
const grouped = Object.groupBy(list, x => x.role);
Interviewer Note: 💡 Tests candidate's commitment to keeping up with modern standards. Ask about browser support and polyfill strategies.
Security requires multi-layered defensive validation:
element.innerHTML). Sanitize inputs using libraries like DOMPurify and configure strict Content Security Policies (CSP).SameSite=Strict attribute. Attach anti-CSRF tokens to API requests and verify them on the server.Interviewer Note: 💡 Ask: 'Why is LocalStorage unsafe for token storage?' (Answer: It is accessible by any script running on the page, making tokens vulnerable to theft via XSS).
Memoization is an optimization technique that caches function results based on input parameters. If a function is called with cached inputs, it returns the result instantly without re-calculating.
function memoize(fn, resolver) {
const cache = new Map();
return function (...args) {
const key = resolver ? resolver(...args) : JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
Interviewer Note: 💡 A good candidate discusses memory management, noting that unbounded memoization caches can lead to memory leaks, which can be mitigated by using cache eviction strategies (e.g. LRU cache).
V8 optimizes arrays dynamically based on their elements' type:
[1,,3]). This de-optimizes arrays, forcing V8 to check prototype chains for missing values, which degrades performance.Interviewer Note: 💡 Check if the candidate knows to avoid creating sparse arrays or changing array element types dynamically to preserve compiler optimizations.
These updates represent different stages of browser rendering:
width, height, top, or margin forces a reflow, which is expensive because it affects parent and sibling elements.color, visibility, or background-color. Faster than a reflow.requestAnimationFrame().transform and opacity) to bypass reflows and repaints.Interviewer Note: 💡 Verify they understand how layout thrashing degrades frame rates and how to debug it using the Chrome Performance tab.
Before native ES Modules, closures and IIFEs were used to manage scoping:
const/let block scoping replace IIFE module patterns, resolving dependency imports natively at compile time.
const CounterModule = (function () {
let privateCount = 0; // Private state
function increment() { privateCount++; }
function getCount() { return privateCount; }
return {
increment,
count: getCount // Reveal public pointers
};
})();
Interviewer Note: 💡 Ask about advantages: private encapsulation blocks outside tampering, keeping state secure.
A complete deep copy implementation must handle non-JSON types (like Date, Map, RegExp) and prevent infinite recursion by tracking visited objects using a WeakMap cache.
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (hash.has(obj)) return hash.get(obj); // Resolve circular ref
const clone = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
hash.set(obj, clone);
// Handle Map and Set
if (obj instanceof Map) {
obj.forEach((val, key) => clone.set(deepClone(key, hash), deepClone(val, hash)));
return clone;
}
if (obj instanceof Set) {
obj.forEach(val => clone.add(deepClone(val, hash)));
return clone;
}
Reflect.ownKeys(obj).forEach(key => {
clone[key] = deepClone(obj[key], hash);
});
return clone;
}
Interviewer Note: 💡 Evaluate their handling of circular references (without the WeakMap cache, circular references will cause stack overflow errors).
The differences lie in host environments and runtime targets:
Interviewer Note: 💡 Ask the candidate about process.nextTick() in Node.js. (Answer: NextTick executes immediately after the current operation, running before promise microtasks).
A micro-frontend shell requires a decoupled, secure event bus for cross-application communication:
class EventBus {
private listeners = new Map();
publish(event, payload) {
const handlers = this.listeners.get(event);
if (handlers) {
handlers.forEach(fn => fn(payload));
}
}
subscribe(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event).add(callback);
return () => this.listeners.get(event).delete(callback); // Return unsubscribe fn
}
}
Interviewer Note: 💡 Look for the unsubscribe return pattern, which enables simple, clean cleanups in component destruction lifecycle hooks.
While SOLID was created for statically typed languages, its principles are highly relevant to JavaScript development:
Interviewer Note: 💡 Strong candidates explain why composition is preferred over inheritance (Prototype Chain) in JavaScript.
An offline-first architecture uses service workers and local databases to handle network connectivity shifts:
Interviewer Note: 💡 Check if the candidate discusses conflict resolution strategies (e.g. Last-Write-Wins, client-server reconciliation) when syncing offline modifications.
A performance library must capture browser metric shifts without impacting page loading performance:
navigator.sendBeacon(). This sends data asynchronously in the background when the user closes the page, preventing request cancellation.
// Listen for cumulative layout shifts (CLS)
let clsScore = 0;
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
}
}).observe({ type: 'layout-shift', buffered: true });
// Send results on page exit
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
navigator.sendBeacon('/api/analytics', JSON.stringify({ cls: clsScore }));
}
});
Interviewer Note: 💡 Verify they understand why visibilitychange is preferred over beforeunload for sending final analytics data.
Building a compiler requires three main pipeline steps:
Interviewer Note: 💡 Look for candidate's knowledge of tree traversal algorithms (Recursive Descent parsing) to build AST representations.
A secure authentication flow requires splitting token responsibilities:
Authorization: Bearer header.HttpOnly, Secure, SameSite=Strict cookie, making it inaccessible to client-side scripts (XSS proof).Interviewer Note: 💡 Ask about CSRF mitigation for the refresh token cookie (SameSite validation and double-submit anti-CSRF token verification).
60fps rendering requires drawing a new frame every 16.6ms. To prevent rendering lag:
OffscreenCanvas inside a Web Worker, freeing up the main thread to handle user interactions.requestAnimationFrame().Interviewer Note: 💡 Look for candidate's strategies to minimize garbage collection runs inside the rendering loop.
A DI container resolves class dependencies recursively based on registered token maps:
class Injector {
private registry = new Map();
private singletons = new Map();
register(token, klass) {
this.registry.set(token, klass);
}
resolve(token) {
if (this.singletons.has(token)) return this.singletons.get(token);
const klass = this.registry.get(token);
// Determine constructor dependencies recursively
const instance = new klass();
this.singletons.set(token, instance);
return instance;
}
}
Interviewer Note: 💡 Ask the candidate how they would detect circular dependencies (e.g. keeping track of resolved tokens in a stack during instantiation).
By default, Array.prototype.sort() converts elements to strings and compares their UTF-16 code unit values.
Because "10" starts with "1", it is sorted before "2" alphabetically.
Solution: Pass a custom comparison function: arr.sort((a, b) => a - b).
Interviewer Note: 💡 Check if the candidate knows that sort() mutates the original array in place.
These properties serve different roles in prototype inheritance:
new.__proto__ at runtime alters an object's prototype chain dynamically. This invalidates all optimized machine code and shapes (hidden classes) cached by V8, forcing execution back to the slow bytecode interpreter.Interviewer Note: 💡 Use Object.create() to configure prototypes at creation time instead of modifying __proto__ at runtime.
In ECMAScript, NaN (Not-a-Number) represents invalid numerical calculations, but its primitive type is still numeric.
According to IEEE 754 floating-point standards, NaN compares unequally to all values, including itself.
Solution: Use Number.isNaN(val), which verifies if the value is NaN without coercion. Avoid global isNaN() because it coerces inputs to numbers, producing false positives (e.g. isNaN('hello') is true).
Interviewer Note: 💡 Ask the candidate how Object.is() handles NaN. (Answer: Object.is(NaN, NaN) returns true).
JavaScript numbers are stored as 64-bit floating-point values according to IEEE 754 standards.
Values like 0.1 and 0.2 cannot be represented exactly in binary fractional notation, which leads to rounding errors (e.g. 0.1 + 0.2 yields 0.30000000000000004).
Solutions: Compare differences against a small tolerance value: Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON, or multiply values to integers before calculations. Use libraries like decimal.js for financial operations.
Interviewer Note: 💡 Essential for financial applications. Candidates must know that precision limits affect calculations.
These variables handle scope and binding differently inside loops:
Interviewer Note: 💡 A classic scope question. Look for a clear distinction between block scope and function scope.
This unexpected result is caused by argument mismatches between the two functions:
map() passes three arguments to its callback: (value, index, array).parseInt() accepts two arguments: (string, radix).parseInt('1', 0) → Radix 0 defaults to base 10 (returns 1).parseInt('2', 1) → Radix 1 is invalid (returns NaN).parseInt('3', 2) → '3' is invalid in binary (returns NaN).
Fix: Explicitly pass arguments: arr.map(num => parseInt(num, 10)).
Interviewer Note: 💡 Demonstrates why you should avoid passing functions directly to map callbacks if they accept optional arguments.
The Temporal Dead Zone (TDZ) is the period from the start of a block scope until the variable is initialized.
typeof safely returns "undefined".typeof) before its initialization throws a ReferenceError.Interviewer Note: 💡 Check if the candidate knows that let/const declarations are hoisted but not initialized.
The JSON standard only supports basic data types (strings, numbers, booleans, objects, arrays, and null).
Because functions, symbols, and undefined values are not part of the JSON specification, JSON.stringify() strips them from object properties (or converts them to null inside arrays).
Serialization: Pass a custom replacer function as the second argument to JSON.stringify() to format these values manually. For example, convert functions to string templates.
Interviewer Note: 💡 Ask about the toJSON() method on objects, which override default stringify behavior.
A single-page application experiences slow performance over time. Heap snapshots show that DOM nodes deleted from the page are still retained in memory.
Detached DOM nodes occur when an HTML element is removed from the DOM tree, but a JavaScript reference (like an active event listener or global variable) still points to it. Because of this reference, the garbage collector cannot reclaim the memory, leading to a memory leak.
removeEventListener before removing elements from the DOM.WeakRef or WeakMap) when binding metadata to DOM elements.Interviewer Note: 💡 Check if the candidate knows how to locate detached DOM nodes using the Chrome DevTools Memory panel.
Users typing in a search bar report that the results shown sometimes match older search queries rather than their final query.
API requests resolve at different times depending on network latency.
If an earlier request (e.g. search for 'abc') takes longer than a subsequent request (e.g. search for 'abcd'), the first request will resolve last, overwriting the UI with outdated results.
AbortController.switchMap operator to automatically cancel previous request streams.Interviewer Note: 💡 A common async bug. Candidates should discuss cancellation patterns rather than just debouncing inputs.
A dashboard application freezes for several seconds when loading heavy reports, making the page unresponsive to clicks.
JSON.parse() is synchronous and runs on the browser's main thread.
Parsing massive JSON payloads (e.g. 50MB reports) blocks the main thread, stopping the event loop and freezing user interactions.
Interviewer Note: 💡 Check if the candidate understands main-thread blocking behaviors and how Web Workers isolate executions.
An image-sharing portal frequently crashes on mobile browsers (especially Safari on iOS) when loading high-resolution images.
Creating image object URLs using URL.createObjectURL(blob) caches the blob data in memory.
Unlike standard variables, these URL references are not garbage-collected automatically when DOM elements are removed. If they are not revoked, memory usage accumulates, crashing mobile browsers with strict memory limits.
URL.revokeObjectURL(url) once the image element has finished loading.Interviewer Note: 💡 Tests mobile performance awareness. Candidates must know that memory resources are highly constrained on mobile platforms.
A security audit reveals that a malicious script injected via an XSS exploit was able to steal user session tokens and hijack accounts.
The application stored JWT session tokens in LocalStorage.
Because LocalStorage is accessible by any script running on the page, the XSS exploit was able to read the token using localStorage.getItem('token') and send it to an attacker's server.
HttpOnly cookies to prevent access via JavaScript.Interviewer Note: 💡 Candidates should talk about security trade-offs between local storage and cookies.
A data visualization app freezes and crashes with a RangeError: Maximum call stack size exceeded when copying complex data models.
The deep copy function recursively copies nested properties. If an object contains a circular reference (pointing back to itself or a parent element), the function recurses indefinitely, exhausting the call stack.
WeakMap cache. If an object has already been copied, return the cached clone instead of recursing again.structuredClone(), which handle circular references automatically.Interviewer Note: 💡 Check if the candidate knows how recursive calls exhaust stack limits and how caches prevent infinite loops.
Write a throttle function that restricts execution rate, support options for leading (run at the start of the window) and trailing (run at the end of the window) executions.
function throttle(fn, wait, options = {}) {
let timerId = null;
let lastArgs = null;
let lastThis = null;
let lastCallTime = 0;
const invoke = () => {
fn.apply(lastThis, lastArgs);
lastCallTime = Date.now();
timerId = null;
};
return function (...args) {
lastArgs = args;
lastThis = this;
const now = Date.now();
const remaining = wait - (now - lastCallTime);
if (remaining <= 0) {
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
if (options.leading !== false) {
invoke();
}
} else if (!timerId && options.trailing !== false) {
timerId = setTimeout(invoke, remaining);
}
};
}
Interviewer Note: 💡 An advanced functional programming challenge. Pay close attention to timing, parameters passing, and context binding.
Implement a queue manager that accepts an array of asynchronous task factories and executes them, ensuring that no more than N tasks run concurrently.
async function limitConcurrency(tasks, limit) {
const results = [];
const executing = new Set();
for (const [index, task] of tasks.entries()) {
const p = Promise.resolve().then(() => task()).then(res => {
results[index] = res;
executing.delete(p);
});
executing.add(p);
if (executing.size >= limit) {
await Promise.race(executing); // Wait for any task to finish
}
}
await Promise.all(executing); // Wait for remaining tasks
return results;
}
Interviewer Note: 💡 Highly relevant for performance. Shows how to avoid overloading APIs when sending concurrent fetch requests.
Create a pub/sub event dispatcher class supporting on(), off(), once(), and emit() methods.
class EventEmitter {
constructor() {
this.events = new Map();
}
on(name, listener) {
if (!this.events.has(name)) this.events.set(name, new Set());
this.events.get(name).add(listener);
return this;
}
emit(name, ...args) {
const listeners = this.events.get(name);
if (listeners) {
listeners.forEach(fn => fn(...args));
}
}
off(name, listener) {
const listeners = this.events.get(name);
if (listeners) {
listeners.delete(listener);
}
}
once(name, listener) {
const wrapper = (...args) => {
this.off(name, wrapper);
listener(...args);
};
this.on(name, wrapper);
}
}
Interviewer Note: 💡 Check if their once() implementation correctly unsubscribes when triggered, preventing subsequent runs.
Write a recursive function that flattens a nested object structure, concatenating nested keys using dot notation.
function flattenObject(obj, prefix = '') {
const result = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
Object.assign(result, flattenObject(obj[key], newKey));
} else {
result[newKey] = obj[key];
}
}
}
return result;
}
Interviewer Note: 💡 Look for proper hasOwnProperty checks to avoid iterating over inherited prototype properties.
Promise.allSettled() waits for all promises to resolve or reject, returning an array of objects detailing the outcome of each promise.
To implement it using Promise.all(), map input promises to wrapper promises that resolve successfully, returning status and value payloads.
function allSettled(promises) {
const wrappedPromises = promises.map(p =>
Promise.resolve(p).then(
value => ({ status: 'fulfilled', value }),
reason => ({ status: 'rejected', reason })
)
);
return Promise.all(wrappedPromises);
}
Interviewer Note: 💡 Ask the candidate why they wrap promises in Promise.resolve() (Answer: to handle non-promise raw values correctly).
Implement these methods on Array.prototype without using built-in array methods. Use loops and respect original argument signatures.
Array.prototype.myMap = function(callback, thisArg) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (i in this) {
result[i] = callback.call(thisArg, this[i], i, this);
}
}
return result;
};
Interviewer Note: 💡 Look for sparse array safety (checking if index 'i' exists in 'this' to skip holes in arrays).
Using the delete keyword on object properties de-optimizes property accesses in V8:
Instead of deleting, assign properties to undefined or use destructuring to create a new object shape without the target property.
Interviewer Note: 💡 V8 engine performance details. AI models often suggest delete to clear properties, ignoring hidden class invalidation hazards.
V8 uses **Inline Caching (IC)** to optimize property accesses by caching property offsets for specific hidden classes (shapes).
Interviewer Note: 💡 Check if the candidate knows to instantiate objects with identical structures and property orders.
A generator maintains a reference to its local execution scope (closure) while it is paused. If an asynchronous loop pulls values from a generator but exits early (e.g. via break or return) without executing to completion, the generator remains suspended. Because it is suspended, V8 preserves its entire scope closure (including variables, arguments, and in-flight promises) in memory, creating a memory leak.
Resolution: Always call generator.return() in cleanup blocks or use for await...of loops, which trigger generator disposal automatically on break.
Interviewer Note: 💡 Excellent test for async generator closures. AI tools often omit generator cleanup when designing dynamic pipelines.
Altering prototypes at runtime using Object.setPrototypeOf() is a slow operation in all modern JavaScript engines.
It invalidates cached prototype chains and object shape lookups throughout the application. All functions that have optimized access to the object are de-optimized and compiled back to slower execution paths.
Alternative: Define prototype structures at creation time using Object.create() instead of modifying them later.
Interviewer Note: 💡 Tests deep runtime optimizations. Candidates must explain shape invalidations across V8 caches.
Calling eval() or new Function() compiles strings to machine code dynamically at runtime.
Executing this in a loop prevents V8's optimizing compilers (TurboFan) from performing compile-time optimizations (like inline caching, scope parsing, or dead-code elimination) because scopes and variables change on every run.
Interviewer Note: 💡 Explain why dynamically parsed code cannot be statically optimized by V8 compilers.
In older V8 engines, passing the arguments object outside a function (e.g. Array.prototype.slice.call(arguments)) caused 'arguments leakage'.
Because the arguments object provides dynamic access to local scope properties, passing it outside prevented V8 from optimizing variable allocations on the stack, forcing it to allocate scope on the heap instead.
Modern V8: The optimizing compiler now supports rest parameters (...args) natively, which optimizes array allocations without heap escape analysis issues.
Interviewer Note: 💡 Tests compilation history. Candidates should explain why rest syntax is preferred over the legacy arguments object.
Array.from() builds a complete, contiguous array in memory synchronously.
For massive lengths, this blocks the main thread during allocation and initialization, freezing user interactions.
Generators: By yielding values lazily using generator functions, you process elements one at a time without allocating large arrays in memory, keeping execution light and responsive.
Interviewer Note: 💡 Look for candidate's strategies to process large datasets without main-thread blocking.
Historically, V8's optimizing compiler (Crankshaft) did not support try/catch blocks. Any function containing a try/catch block was marked as de-optimized and could not be inlined into parent call stacks. This introduced function call overhead and restricted compiler optimizations.
Modern V8: The TurboFan compiler supports try/catch natively, but keeping try/catch blocks small remains a best practice to avoid de-optimizations in hot loops.
Interviewer Note: 💡 Tests V8 engine history. Ask how they isolate error-prone code blocks from hot path calculations.
V8 allocates arrays as contiguous memory slots to speed up index lookups.
If you assign an element to a large index (e.g. arr[1000000] = 1) on a small array, allocating a million empty memory slots is highly inefficient.
V8 converts the array to **Dictionary Mode**, storing index-value pairs as a hash table in memory. While this saves memory, it makes index lookups significantly slower.
Interviewer Note: 💡 Tests deep array optimization strategies in V8. Candidates should explain why sparse arrays degrade performance.
A standard Map maintains strong references to both its keys and values.
If you delete all variables referencing a key object, the Map still holds a reference to it in memory.
As a result, the key object and its value cannot be garbage-collected, which leads to a memory leak. Use a WeakMap instead to allow garbage collection.
Interviewer Note: 💡 Verify they understand the difference between strong and weak references.
Browsers preserve references to objects logged in the developer console so users can inspect their properties dynamically. Because the console holds active references, the logged objects cannot be garbage-collected. Logging objects repeatedly inside high-frequency loops causes memory usage to grow continuously, which can crash the browser tab.
Interviewer Note: 💡 A common debugging trap. Candidates should disable verbose console logs in production environments.
The event loop runs microtasks continuously until the microtask queue is empty. If an asynchronous function repeatedly schedules new microtasks (e.g. recursive Promise resolves), the event loop remains stuck in the microtask phase. Because it is stuck, it never reaches the macrotask or rendering phases, which freezes the browser and stops layout updates.
Macrotask Scheduling: Using setTimeout yields execution control back to the event loop, allowing the browser to render updates between iterations.
Interviewer Note: 💡 Check if the candidate knows how microtask queues are cleared compared to macrotasks.
These module systems handle imports and bindings differently:
require() returns a copy of the exported object. If circular dependency occurs, require returns an incomplete, partially evaluated object, leading to undefined exports.Interviewer Note: 💡 Look for candidate's understanding of static compile-time linking (ESM) vs runtime execution (CommonJS).
Spectre side-channel attacks exploit timing differences in CPU caches to read secure memory values. Attackers require high-precision timers to execute these exploits.
To mitigate this risk, browser vendors reduce the precision of performance.now() (e.g. rounding timestamps to the nearest 5-10 microseconds) and introduce random jitter to prevent micro-timing attacks.
Interviewer Note: 💡 Tests browser security and CPU side-channel awareness. An advanced topic that highlights browser-level security.
Creating functions or closures inside a high-frequency rendering loop (running 60 times per second) allocates new function objects in memory on every frame. This rapid object allocation quickly fills the young generation memory space, triggering frequent scavenger garbage collection runs (GC thrashing). This blocks the main thread, causing frame rate drops (jank) and laggy animations.
Resolution: Define helper functions outside the rendering loop and pass necessary data as arguments to avoid creating closures on every frame.
Interviewer Note: 💡 A critical performance optimization topic. Candidates should explain why allocations inside rendering loops degrade performance.
Calling Function.prototype.bind.call() creates direct, optimized context bindings in the engine.
This avoids the overhead of creating additional JavaScript closures and wrapping nested functions, allowing V8 compilers to inline function executions directly.
Interviewer Note: 💡 Tests deep runtime optimizations. Look for candidate's knowledge of function call structures in V8.
According to ECMAScript specifications, object properties are ordered based on key type during iteration (e.g. Object.keys(), JSON.stringify()):
The Trap: Mixing numeric and string keys can lead to unexpected property ordering during serializations.
Interviewer Note: 💡 Ask the candidate how they would preserve insertion order when using numeric keys (Answer: Use a Map instead of an Object).
Yes. The guide covers beginner, intermediate, senior, and advanced JavaScript concepts commonly asked in interviews.
Yes. Topics include ES6+, async/await, promises, modules, destructuring, optional chaining, and other modern JavaScript features.
Yes. It covers closures, hoisting, scope, prototypes, this keyword, event loop, execution context, and memory management.
Yes. The guide includes coding challenges, debugging questions, DOM manipulation, and real-world problem-solving exercises.
Yes. It includes browser APIs, DOM events, asynchronous programming, performance optimization, and topics frequently asked in frontend interviews.