JavaScript Interview Questions and Answers
ECMAScript fundamentals, closures, prototypes, event loop, async patterns, DOM manipulation and modern ES6+ features.
Whether you are preparing for entry-level JavaScript interview questions for freshers or senior software engineer interview questions addressing concurrency, scalability, and system architecture, this track provides peer-reviewed model answers with syntax walkthroughs, edge cases, and practical interview tips.
1 What is the currying function? Hard
Currying is the process of transforming a function with multiple arguments into a sequence of nested functions, each accepting only one argument at a time.
This concept is named after mathematician Haskell Curry, and is commonly used in functional programming to enhance modularity and reuse.
## Before Currying (Normal n-ary Function)
const multiArgFunction = (a, b, c) => a + b + c;
console.log(multiArgFunction(1, 2, 3)); // Output: 6
This is a standard function that takes three arguments at once.
## After Currying (Unary Function Chain)
const curryUnaryFunction = (a) => (b) => (c) => a + b + c;
console.log(curryUnaryFunction(1)); // Returns: function (b) => ...
console.log(curryUnaryFunction(1)(2)); // Returns: function (c) => ...
console.log(curryUnaryFunction(1)(2)(3)); // Output: 6
Each function in the chain accepts one argument and returns the next function, until all arguments are provided and the final result is computed.
## Benefits of Currying
- Improves code reusability
→ You can partially apply functions with known arguments.
- Enhances functional composition
→ Easier to compose small, pure functions.
- Encourages clean, modular code
→ You can split logic into smaller single-responsibility functions.
2 What is a WeakMap? Hard
A WeakMap object is a collection of key/value pairs in which the keys are weakly referenced. In this case, keys must be objects and the values can be arbitrary values. The syntax looks like the following:
new WeakMap([iterable]);
Let's see the below example to explain it's behavior,
var ws = new WeakMap();
var user = {};
ws.set(user);
ws.has(user); // true
ws.delete(user); // removes user from the map
ws.has(user); // false, user has been removed
3 What are the differences between WeakMap and Map? Hard
The main difference is that references to key objects in Map are strong while references to key objects in WeakMap are weak. i.e, A key object in WeakMap can be garbage collected if there is no other reference to it.
Other differences are,
Mapcan store any key type whereasWeakMapcan store only collections of key objectsWeakMapdoes not have size property unlikeMapWeakMapdoes not have methods such as clear, keys, values, entries, forEach.WeakMapis not iterable.
4 List down the collection of methods available on WeakMap Hard
Below are the list of methods available on WeakMap,
set(key, value): Sets the value for the key in theWeakMapobject. Returns theWeakMapobject.delete(key): Removes any value associated to the key.has(key): Returns a Boolean asserting whether a value has been associated to the key in theWeakMapobject or not.get(key): Returns the value associated to the key, or undefined if there is none.
Let's see the functionality of all the above methods in an example,
var weakMapObject = new WeakMap();
var firstObject = {};
var secondObject = {};
// set(key, value)
weakMapObject.set(firstObject, "John");
weakMapObject.set(secondObject, 100);
console.log(weakMapObject.has(firstObject)); //true
console.log(weakMapObject.get(firstObject)); // John
weakMapObject.delete(secondObject);
5 What is the event loop? Hard
The event loop is a process that continuously monitors both the call stack and the event queue and checks whether or not the call stack is empty. If the call stack is empty and there are pending events in the event queue, the event loop dequeues the event from the event queue and pushes it to the call stack. The call stack executes the event, and any additional events generated during the execution are added to the end of the event queue.
Note: The event loop allows Node.js to perform non-blocking I/O operations, even though JavaScript is single-threaded, by offloading operations to the system kernel whenever possible. Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background.
6 What is V8 JavaScript engine? Hard
V8 is an open source high-performance JavaScript engine used by the Google Chrome browser, written in C++. It is also being used in the node.js project. It implements ECMAScript and WebAssembly, and runs on Windows 7 or later, macOS 10.12+, and Linux systems that use x64, IA-32, ARM, or MIPS processors.
Note: It can run standalone, or can be embedded into any C++ application.
7 What are tasks in event loop? Hard
A task is any javascript code/program which is scheduled to be run by the standard mechanisms such as initially starting to run a program, run an event callback, or an interval or timeout being fired. All these tasks are scheduled on a task queue.
Below are the list of use cases to add tasks to the task queue,
- When a new javascript program is executed directly from console or running by the
<script>element, the task will be added to the task queue. - When an event fires, the event callback added to task queue
- When a setTimeout or setInterval is reached, the corresponding callback added to task queue
8 What is microtask? Hard
A microtask is a type of JavaScript callback that is scheduled to run immediately after the currently executing script and before the next event loop tick. Microtasks are executed after the current task completes and before any new tasks (macrotasks) are run. This ensures a fast and predictable update cycle.
Common sources of microtasks stored in the microtask queue include:
- Promises:
When a Promise is resolved or rejected, its .then(), .catch(), and .finally() callbacks are placed in the microtask queue.
Promise.resolve().then(() => {
console.log('Microtask from a Promise');
});
- queueMicrotask():
A method that explicitly schedules a function to be run in the microtask queue.
queueMicrotask(() => {
console.log('Microtask from queueMicrotask');
});
- MutationObserver callbacks:
Observers changes in the DOM and triggers a callback as a microtask.
const observer = new MutationObserver(() => {
console.log('Microtask from MutationObserver');
})
observer.observe(document.body, {childList: true});
- await:
Await internally uses Promises, so the code after await is scheduled as a microtask.
async function asyncFunction() {
await null;
console.log('Microtask from Await'); // Schedule this code as microtask
}
Note: All of these microtasks are processed in the same turn of the event loop.
9 What are different event loops? Hard
In JavaScript, there are multiple event loops that can be used depending on the context of your application. The most common event loops are:
- The Browser Event Loop
- The Node.js Event Loop
- Browser Event Loop: The Browser Event Loop is used in client-side JavaScript applications and is responsible for handling events that occur within the browser environment, such as user interactions (clicks, keypresses, etc.), HTTP requests, and other asynchronous actions.
- The Node.js Event Loop is used in server-side JavaScript applications and is responsible for handling events that occur within the Node.js runtime environment, such as file I/O, network I/O, and other asynchronous actions.
10 What is the purpose of queueMicrotask? Hard
The queueMicrotask function is used to schedule a microtask, which is a function that will be executed asynchronously in the microtask queue. The purpose of queueMicrotask is to ensure that a function is executed after the current task has finished, but before the browser performs any rendering or handles user events.
Example:
console.log("Start"); //1
queueMicrotask(() => {
console.log("Inside microtask"); // 3
});
console.log("End"); //2
By using queueMicrotask, you can ensure that certain tasks or callbacks are executed at the earliest opportunity during the JavaScript event loop, making it useful for performing work that needs to be done asynchronously but with higher priority than regular setTimeout or setInterval callbacks.
11 What is a microTask queue? Hard
Microtask Queue is the new queue where all the tasks initiated by promise objects get processed before the callback queue.
The microtasks queue are processed before the next rendering and painting jobs. But if these microtasks are running for a long time then it leads to visual degradation.
12 What is a Proper Tail Call? Hard
First, we should know about tail call before talking about "Proper Tail Call". A tail call is a subroutine or function call performed as the final action of a calling function. Whereas Proper tail call(PTC) is a technique where the program or code will not create additional stack frames for a recursion when the function call is a tail call.
For example, the below classic or head recursion of factorial function relies on stack for each step. Each step need to be processed upto n * factorial(n - 1)
function factorial(n) {
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(5)); //120
But if you use Tail recursion functions, they keep passing all the necessary data it needs down the recursion without relying on the stack.
function factorial(n, acc = 1) {
if (n === 0) {
return acc;
}
return factorial(n - 1, n * acc);
}
console.log(factorial(5)); //120
The above pattern returns the same output as the first one. But the accumulator keeps track of total as an argument without using stack memory on recursive calls.
13 What are the possible reasons for memory leaks? Hard
Memory leaks can lead to poor performance, slow loading times and even crashes in web applications. Some of the common causes of memory leaks are listed below,
- The execessive usage of global variables or omitting the
varkeyword in local scope. - Forgetting to clear the timers set up by
setTimeoutorsetInterval. - Closures retain references to variables from their parent scope, which leads to variables might not garbage collected even they are no longer used.
14 What are the optimization techniques of V8 engine? Hard
V8 engine uses the below optimization techniques.
- Inline expansion: It is a compiler optimization by replacing the function calls with the corresponding function blocks.
- Copy elision: This is a compiler optimization method to prevent expensive extra objects from being duplicated or copied.
- Inline caching: It is a runtime optimization technique where it caches the execution of older tasks those can be lookup while executing the same task in the future.
15 What are generator functions and how do they work? Hard
Generator functions are special functions that can pause execution and resume later, allowing them to produce a sequence of values over time instead of computing them all at once.
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
Key features:
- Lazy evaluation:
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
- Two-way communication:
function* twoWay() {
const x = yield 'First';
yield `Got: ${x}`;
}
const gen = twoWay();
console.log(gen.next()); // { value: 'First', done: false }
console.log(gen.next('data')); // { value: 'Got: data', done: false }
- Delegating to other generators:
function* gen1() { yield 1; yield 2; }
function* gen2() {
yield* gen1();
yield 3;
}
Practical uses: iterating large datasets, implementing custom iterators, managing async flows (though async/await is now preferred).
16 What is tail call optimization and does JavaScript support it? Hard
Tail call optimization (TCO) is a technique where a function call in tail position (the last operation before returning) reuses the current stack frame instead of creating a new one, preventing stack overflow in recursive functions.
Tail call example:
// Tail call - last operation is the recursive call
function factorial(n, acc = 1) {
if (n <= 1) return acc;
return factorial(n - 1, n * acc); // Tail call
}
// Not a tail call - multiplication happens after the recursive call
function factorialNonTail(n) {
if (n <= 1) return 1;
return n * factorialNonTail(n - 1); // NOT a tail call
}
JavaScript TCO support:
- Specified in ES6 (ES2015) but poorly supported
- Only Safari/JavaScriptCore implements it
- Chrome V8 and Firefox SpiderMonkey do not support it
- Most JavaScript engines ignore TCO
Workaround - trampolining:
function trampoline(fn) {
while (typeof fn === 'function') {
fn = fn();
}
return fn;
}
function factorial(n, acc = 1) {
if (n <= 1) return acc;
return () => factorial(n - 1, n * acc);
}
const result = trampoline(() => factorial(100000)); // Won't stack overflow
Workaround - iteration instead of recursion:
// Recursive (can cause stack overflow)
function sumRecursive(arr, index = 0, acc = 0) {
if (index >= arr.length) return acc;
return sumRecursive(arr, index + 1, acc + arr[index]);
}
// Iterative (safe)
function sumIterative(arr) {
let sum = 0;
for (const num of arr) {
sum += num;
}
return sum;
}
Checking for TCO:
function checkTCO(n) {
if (n === 0) return true;
return checkTCO(n - 1);
}
try {
checkTCO(100000);
console.log('TCO supported');
} catch (e) {
if (e instanceof RangeError) {
console.log('TCO not supported');
}
}
Best practice: Don't rely on TCO in JavaScript. Use iteration or trampolining for deep recursion.
17 What are the differences between SharedArrayBuffer and ArrayBuffer? Hard
SharedArrayBuffer and ArrayBuffer are both fixed-length binary data buffers, but SharedArrayBuffer allows sharing memory between multiple workers/threads.
ArrayBuffer (not shared):
// Regular ArrayBuffer
const buffer = new ArrayBuffer(16);
const view = new Int32Array(buffer);
view[0] = 42;
console.log(view[0]); // 42
// Transferable but not shared
worker.postMessage(buffer, [buffer]);
// buffer is now neutered (length = 0)
SharedArrayBuffer (shared memory):
// Main thread
const sharedBuffer = new SharedArrayBuffer(16);
const sharedView = new Int32Array(sharedBuffer);
sharedView[0] = 42;
// Send to worker (shared, not transferred)
worker.postMessage(sharedBuffer);
// Both main thread and worker can access the same memory
sharedView[0] = 100; // Worker will see this change
Key differences:
| ArrayBuffer | SharedArrayBuffer |
|-------------|-------------------|
| Single context only | Multiple contexts (workers/threads) |
| Transferred (moved) between workers | Shared between workers |
| No synchronization needed | Requires Atomics for safe access |
| Always available | Requires secure context (HTTPS) |
| Original becomes neutered after transfer | Original remains valid |
Using Atomics with SharedArrayBuffer:
// Main thread
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
worker.postMessage(sab);
// Atomic operations
Atomics.store(view, 0, 42); // Write atomically
Atomics.add(view, 0, 10); // Add 10 atomically
const value = Atomics.load(view, 0); // Read atomically
// Wait/notify pattern
Atomics.wait(view, 0, 0); // Wait until value changes
Atomics.notify(view, 0, 1); // Wake one waiter
Worker communication example:
// Main thread
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 2);
const sharedArray = new Int32Array(sharedBuffer);
const worker = new Worker('worker.js');
worker.postMessage({ buffer: sharedBuffer });
// Increment counter atomically
setInterval(() => {
const oldValue = Atomics.add(sharedArray, 0, 1);
console.log('Main thread incremented to:', oldValue + 1);
}, 1000);
// worker.js
self.onmessage = function(e) {
const sharedArray = new Int32Array(e.data.buffer);
setInterval(() => {
const oldValue = Atomics.add(sharedArray, 1, 1);
console.log('Worker incremented to:', oldValue + 1);
}, 1000);
};
Security requirements for SharedArrayBuffer:
// Requires these headers:
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp
// Check availability
if (typeof SharedArrayBuffer !== 'undefined') {
console.log('SharedArrayBuffer is available');
} else {
console.log('SharedArrayBuffer is not available');
}
18 How do you prevent prototype pollution attacks in JavaScript? Hard
Prototype pollution is a security vulnerability where attackers inject properties into Object.prototype, affecting all objects in the application.
Vulnerable code:
function merge(target, source) {
for (let key in source) {
target[key] = source[key];
}
return target;
}
// Attack payload
const malicious = JSON.parse('{"__proto__": {"polluted": "yes"}}');
merge({}, malicious);
console.log({}.polluted); // "yes" - all objects are polluted!
Prevention 1: Use Object.create(null):
// Create objects without prototype
const safeObj = Object.create(null);
safeObj.__proto__ = { polluted: 'yes' };
console.log(safeObj.polluted); // undefined
// For configuration objects
const config = Object.create(null);
config.apiUrl = 'https://api.example.com';
Prevention 2: Check for dangerous keys:
function safeMerge(target, source) {
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
for (let key in source) {
if (dangerousKeys.includes(key)) {
continue; // Skip dangerous keys
}
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
return target;
}
Prevention 3: Use Map instead of objects:
const safeMap = new Map();
safeMap.set('__proto__', 'value');
// No pollution risk
Prevention 4: Freeze Object.prototype:
Object.freeze(Object.prototype);
Object.freeze(Object);
// Now pollution attempts will fail
Object.prototype.polluted = 'no';
console.log({}.polluted); // undefined
Prevention 5: Validate object paths:
function setDeepProperty(obj, path, value) {
const parts = path.split('.');
const dangerous = ['__proto__', 'constructor', 'prototype'];
// Validate each part of the path
if (parts.some(part => dangerous.includes(part))) {
throw new Error('Invalid property path');
}
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
if (!(parts[i] in current)) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
}
Prevention 6: Use libraries with protection:
// Use lodash's merge with customizer
const _ = require('lodash');
function safeMergeCustomizer(objValue, srcValue, key) {
const dangerous = ['__proto__', 'constructor', 'prototype'];
if (dangerous.includes(key)) {
return objValue; // Keep original value
}
}
const result = _.mergeWith({}, source, safeMergeCustomizer);
Prevention 7: Schema validation:
const Ajv = require('ajv');
const ajv = new Ajv();
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' }
},
additionalProperties: false // Reject unknown properties
};
const validate = ajv.compile(schema);
function safeProcess(data) {
if (!validate(data)) {
throw new Error('Invalid data');
}
return data;
}
Prevention 8: JSON.parse with reviver:
function safeJSONParse(text) {
return JSON.parse(text, (key, value) => {
const dangerous = ['__proto__', 'constructor', 'prototype'];
if (dangerous.includes(key)) {
return undefined; // Filter out dangerous keys
}
return value;
});
}
const safe = safeJSONParse('{"__proto__": {"polluted": "yes"}}');
console.log({}.polluted); // undefined
19 What is the Atomics API and when should it be used? Hard
The Atomics API provides atomic operations on SharedArrayBuffer, ensuring thread-safe access to shared memory in multi-threaded JavaScript (workers).
Basic atomic operations:
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 4);
const view = new Int32Array(sab);
// Atomic store - write a value
Atomics.store(view, 0, 42);
// Atomic load - read a value
const value = Atomics.load(view, 0); // 42
// Atomic add - add and return old value
const oldValue = Atomics.add(view, 0, 10); // returns 42, view[0] is now 52
// Atomic sub - subtract
Atomics.sub(view, 0, 2); // view[0] is now 50
// Atomic exchange - swap values
const prev = Atomics.exchange(view, 0, 100); // returns 50, view[0] is now 100
// Compare and exchange
const replaced = Atomics.compareExchange(view, 0, 100, 200);
// If view[0] === 100, set it to 200 and return 100
// Otherwise, return current value
Wait and notify (worker synchronization):
// Main thread
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const view = new Int32Array(sab);
worker.postMessage(sab);
// Wait for worker to set value to 1
Atomics.wait(view, 0, 0); // Blocks until view[0] !== 0
console.log('Worker has finished');
// Worker thread
self.onmessage = function(e) {
const view = new Int32Array(e.data);
// Do some work
performTask();
// Signal completion
Atomics.store(view, 0, 1);
Atomics.notify(view, 0, 1); // Wake up one waiting thread
};
Mutex implementation:
class Mutex {
constructor(sab, index) {
this.sab = sab;
this.index = index;
}
lock() {
const view = new Int32Array(this.sab);
while (true) {
const oldValue = Atomics.compareExchange(view, this.index, 0, 1);
if (oldValue === 0) {
return; // Successfully acquired lock
}
Atomics.wait(view, this.index, 1); // Wait if locked
}
}
unlock() {
const view = new Int32Array(this.sab);
Atomics.store(view, this.index, 0);
Atomics.notify(view, this.index, 1);
}
}
// Usage
const mutex = new Mutex(sab, 0);
mutex.lock();
try {
// Critical section
criticalOperation();
} finally {
mutex.unlock();
}
Counter with atomic operations:
class AtomicCounter {
constructor() {
this.sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
this.view = new Int32Array(this.sab);
}
increment() {
return Atomics.add(this.view, 0, 1) + 1;
}
decrement() {
return Atomics.sub(this.view, 0, 1) - 1;
}
get value() {
return Atomics.load(this.view, 0);
}
set value(val) {
Atomics.store(this.view, 0, val);
}
}
Available atomic operations:
// Arithmetic
Atomics.add(typedArray, index, value)
Atomics.sub(typedArray, index, value)
// Bitwise
Atomics.and(typedArray, index, value)
Atomics.or(typedArray, index, value)
Atomics.xor(typedArray, index, value)
// Memory
Atomics.load(typedArray, index)
Atomics.store(typedArray, index, value)
Atomics.exchange(typedArray, index, value)
Atomics.compareExchange(typedArray, index, expectedValue, replacementValue)
// Synchronization
Atomics.wait(typedArray, index, value, timeout)
Atomics.notify(typedArray, index, count)
// Utility
Atomics.isLockFree(size)
When to use Atomics:
- Sharing data between web workers
- Implementing locks, semaphores, or other synchronization primitives
- Building concurrent data structures
- High-performance parallel computing
- Avoiding race conditions in shared memory
20 What is an event loop? Hard
The event loop in JavaScript handles asynchronous operations by queuing them up and processing them one by one in a non-blocking way. It checks the event queue continuously and processes the oldest operation first. When an operation is completed, its callback function is executed.
21 What is the difference between the asterisk () and the plus sign (+) in regular expressions? Hard
In regular expressions, the asterisk (\*) matches zero or more occurrences of the preceding character, while the plus sign (+) matches one or more occurrences of the preceding character.
For example, if we want to match the letter "a" followed by zero or more "b" characters, we would use the asterisk in our regular expression like this: /ab\*/. This would match strings like "a", "ab", "abb", "abbb", and so on.
On the other hand, if we want to match the letter "a" followed by one or more "b" characters, we would use the plus sign in our regular expression like this: /ab+/
All 21 questions loaded
Frequently Asked Questions About JavaScript Interviews
What do hiring managers evaluate in JavaScript technical rounds?
Technical interviewers look for foundational fluency, idiomatic syntax, clarity when communicating complex logic, and awareness of performance trade-offs (e.g. memory footprint, render performance, and network latency) in production environments.
What are the best interview tips for practicing JavaScript questions?
Use active recall: summarize each answer in your own words before revealing the model solution. Focus on explaining why a certain approach is chosen rather than just memorizing code syntax.