JavaScript Interview Questions and Answers

ECMAScript fundamentals, closures, prototypes, event loop, async patterns, DOM manipulation and modern ES6+ features.

Practise 10 random 21 peer-reviewed questions
JavaScript Interview Syllabus & Preparation Strategy

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,

  1. Map can store any key type whereas WeakMap can store only collections of key objects
  2. WeakMap does not have size property unlike Map
  3. WeakMap does not have methods such as clear, keys, values, entries, forEach.
  4. WeakMap is not iterable.
4 List down the collection of methods available on WeakMap Hard

Below are the list of methods available on WeakMap,

  1. set(key, value): Sets the value for the key in the WeakMap object. Returns the WeakMap object.
  2. delete(key): Removes any value associated to the key.
  3. has(key): Returns a Boolean asserting whether a value has been associated to the key in the WeakMap object or not.
  4. 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,

  1. 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.
  2. When an event fires, the event callback added to task queue
  3. 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:

  1. 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');
        });
        
  1. queueMicrotask():

A method that explicitly schedules a function to be run in the microtask queue.

          queueMicrotask(() => {
             console.log('Microtask from  queueMicrotask');
           });
         
  1. 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});
         
  1. 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:

  1. The Browser Event Loop
  2. 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,

  1. The execessive usage of global variables or omitting the var keyword in local scope.
  2. Forgetting to clear the timers set up by setTimeout or setInterval.
  3. 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.

  1. Inline expansion: It is a compiler optimization by replacing the function calls with the corresponding function blocks.
  2. Copy elision: This is a compiler optimization method to prevent expensive extra objects from being duplicated or copied.
  3. 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:

  1. Lazy evaluation:
        function* infiniteSequence() {
          let i = 0;
          while (true) {
            yield i++;
          }
        }
        
  1. 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 }
        
  1. 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.

Showing 20 of 21 questions

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.