JavaScript Interview Questions for 5+ Years Experience - Interview Questions and Answers

  • Use module boundaries, lazy-loading, separation of concerns, code-splitting, state management boundaries (e.g., Redux slices/domains).
  • Devise a chunk loading strategy, caching, SSR/CSR options, and performance budgets.
  • Include error boundaries, logging, and A/B feature flags.

Start with automated conversion, incremental class adoption via codemods, maintain behavior parity with tests, slowly remove Prototype-based APIs, adopt bundler for ESM.

Use performance profiling → pinpoint reflows (excessive DOM updates), deep diff cycles; fix by virtualization (windowing), memoizing list items, using requestAnimationFrame, batching updates, optimizing render functions.

Use Service Workers, IndexedDB, background sync. Version items with timestamps, use CRDT or last-write-wins, resolve conflicts in sync layer, queue operations while offline.

Use window.onerror, window.addEventListener('unhandledrejection'), custom error classes, centralized logging to backend, fallback UI/error boundaries, retry logic, circuit breaker.

Sanitize user input, use CSP, escape dynamic DOM insertion, avoid innerHTML. For objects: deep-freeze, avoid untrusted Object.assign, use Object.create(null) for maps, lock prototypes via Object.freeze(Object.prototype).

Delegate to Web Workers, use TypedArrays or Offload computations to wasm or transferable object, update UI via postMessage, show intermediate progress indicators.

Use schema-driven forms, validate using Constraint Validation API or schema (Yup), debounce validation, lazy load validation rules, isolate form state, render dynamically with virtualized inputs.

Use modular architecture with ES modules or a monorepo approach. Employ:

  • Feature-based folder structure
  • Component isolation with clear APIs
  • Shared libraries
  • Linting, testing, and CI/CD pipelines
  • Code ownership per team/module
    Also apply Micro-Frontend Architecture if the teams are highly autonomous.

  • Constructor Function: Uses new, tightly coupled with class semantics.
  • Factory Function: Returns objects, can close over private data, avoids this, more testable.

Prefer factory functions in larger apps for better testability and encapsulation unless class inheritance is crucial.

  • Use promisify (Node.js util.promisify) or manually wrap callbacks into Promises.
  • Apply async/await to make the flow readable.
  • Break code into smaller functions.
  • Use named functions instead of anonymous for clarity.

  • Profile using Chrome DevTools (Performance tab)
  • Use lazy loading, code-splitting, and memoization
  • Avoid unnecessary re-renders (React: React.memo, useMemo)
  • Minimize DOM operations and use requestAnimationFrame for animations
  • Debounce or throttle expensive operations

  • Avoid polling, use WebSockets
  • Use virtual DOM or virtual scrolling
  • Implement client-side caching
  • Use service workers for offline and caching
  • Reduce bundle size (tree-shaking, compression)

  • Build a shared NPM package (internal registry)
  • Use TypeScript for typings and safety
  • Version with SemVer
  • Write documentation and add tests

  • Use Chrome DevTools > Performance > Memory
  • Look for detached DOM nodes or retained closures
  • Check for global variables or event listeners not being cleaned
  • Use WeakMap or WeakRef to avoid strong references

Occurs when long-running tasks (e.g., while loops) block the main thread. Prevent it by:

  • Splitting heavy tasks using setTimeout, requestIdleCallback, or Web Workers

  • Use libraries like Immutable.js or Immer
  • Avoid direct state mutation
  • Use object spread or Object.assign
  • Enforce via linters and TypeScript

  • SSR: Better SEO, faster first paint
  • CSR: Better interactivity, control
    Use SSR for landing pages, marketing, or content-heavy pages. CSR for apps with dynamic content after login.

Use Chrome DevTools → Memory tab → take heap snapshots. Look for:

  • Detached DOM nodes
  • Objects not being garbage collected
  • Closures retaining references
    Fix by cleaning up timers, event listeners, and circular references.

It happens when removed DOM elements are still referenced in JS (e.g., event handlers not cleaned). Use removeEventListener or WeakRef.

Event loop processes:

  • Microtasks (e.g., Promise.then, MutationObserver) before next macrotask
  • Macrotasks (e.g., setTimeout, setInterval) executed after microtasks
    This order affects async behavior.

Use:

  • Web Workers for CPU-heavy tasks
  • setTimeout or requestIdleCallback to chunk tasks
  • Offload to backend if needed

  • XSS (Cross-site scripting): Sanitize user inputs
  • CSRF: Use tokens
  • CSP: Apply Content Security Policy
  • Avoid eval, use strict mode, validate inputs

Use centralized state management (e.g., Redux, Zustand), with immutability and selectors. Modularize state slices per feature.

  • null: Intentional absence of value
  • undefined: Uninitialized or missing value
  • void 0: Always returns undefined, used to protect against reassigned undefined

  • Debounce: Fires once after inactivity (e.g., search input)
  • Throttle: Limits rate of calls (e.g., scroll, resize events)

Use Webpack 5 Module Federation plugin:

  • Expose and consume components remotely
  • Useful in micro-frontend architectures

  • localStorage: Persistent, string-based, 5–10 MB
  • sessionStorage: Per-tab only
  • IndexedDB: Structured data, asynchronous, large capacity

In JS, it's often implemented via custom events or EventEmitter.

class Subject {
  observers = [];
  subscribe(fn) { this.observers.push(fn); }
  notify(data) { this.observers.forEach(fn => fn(data)); }
}

They hold weak references, allowing GC of keys when not used elsewhere — ideal for metadata or private data in classes.

JS runs on a single-threaded event loop. Concurrency is simulated using callbacks, promises, and async/await via the event loop.

Use frameworks like Jest/Mocha with done callback, async/await, or return a Promise from the test function.

Caused by uncontrolled recursion or infinite loops. Fix by:

  • Using iteration
  • Limiting recursion depth
  • Using tail-call optimization (if supported)

Proxy lets you intercept and define custom behavior for fundamental operations (e.g., get, set). Used for:

  • Validation
  • Tracing
  • Virtualization
  • Reactive state (Vue.js)

A recursive function that returns another function’s result directly can reuse stack. Limited browser support (mostly non-strict mode).

Variable and function declarations are moved to the top of their scope.

  • var is hoisted and initialized as undefined
  • let/const are hoisted but uninitialized (TDZ)

It's the phase between entering the scope and variable declaration where accessing a let/const variable throws a ReferenceError.

  • Create unique property keys (avoid collisions)
  • Implement internal object behavior (e.g., iterators, Symbol.iterator)

A closure is a function that retains access to its lexical scope even when the outer function has finished execution.
Real-world uses:

  • Data privacy (e.g., counter module)
  • Currying
  • Memoization
  • Event handlers in loops

  • Use Promise.all cautiously
  • Locking mechanisms or semaphores
  • Implement queue-based execution
  • Avoid shared mutable state in async functions

Use:

  • Chrome DevTools → Performance → Heap snapshot
  • Lighthouse for memory
  • Third-party tools like clinic.js
    Simulate traffic using automated test scripts

Use Object.defineProperty to define non-enumerable, read-only, or getter/setter-based properties for controlled access.

  • Use schema-based validation (e.g., Yup)
  • Controlled components with React
  • Avoid deep nesting in state
  • Use custom hooks for reusability

  • Disable button immediately after first click
  • Debounce or throttle click handler
  • Track state to ignore repeat clicks

Service workers are background scripts that intercept network requests for caching and offline support.
Used in:

  • PWA
  • Background sync
  • Push notifications

  • Configure CORS on backend
  • Use JSONP (legacy) or proxies
  • With credentials: fetch(url, { credentials: 'include' })

Generators (function*) yield values lazily.
Used in:

  • Custom iterators
  • Asynchronous control flows
  • Pause/resume execution scenarios

  • call: Invokes function with arguments
  • apply: Same as call but uses array
  • bind: Returns new function with bound context

It prevents runtime errors when accessing deep nested objects that might be undefined or null.

user?.address?.city // won't throw if user or address is undefined

Use Map when:

  • Keys are not strings
  • Need insertion order
  • High-frequency lookups with complex keys

Modules (ESM, CommonJS) allow encapsulated code, reusability, and dependency management, which improves maintainability in large codebases.

setTimeout and setInterval are managed via the Web APIs and queued into the event loop once the delay is complete. They are not precise timers.

  • Use virtualization (e.g., react-window)
  • Debounce scroll events
  • Avoid re-rendering the full list
  • Use requestAnimationFrame

  • Chrome DevTools → Performance tab
  • Use console.time, Performance.now()
  • Identify bottlenecks (layout thrashing, loops)

  • Never expose secrets in frontend
  • Use short-lived access tokens
  • Store in memory (avoid localStorage)
  • Use HTTP-only secure cookies

==: Type coercion comparison
===: Strict equality
Use === always to avoid unexpected coercion.

  • structuredClone() (modern)
  • JSON.parse(JSON.stringify(obj)) (limited)
  • Recursive custom function or lodash.cloneDeep

class EventBus {
  events = {};
  on(event, fn) { (this.events[event] ||= []).push(fn); }
  emit(event, data) { (this.events[event] || []).forEach(fn => fn(data)); }
}

  • Batch DOM changes
  • Use DocumentFragment
  • Minimize reflows
  • Avoid layout thrashing

  • Use IntersectionObserver
  • Dynamically import components/modules
  • loading="lazy" for images

Transforming a function with multiple arguments into a sequence of unary functions.
Used in functional programming and reusability.

const add = x => y => x + y;

  • Use error tracking (Sentry, Rollbar)
  • Add logs and replay sessions
  • Use source maps
  • Debug based on user data, network logs

  • Use local packages or libraries
  • npm workspaces, Lerna, or Nx
  • Follow semantic versioning internally

Use Chrome DevTools Memory tab to take heap snapshots. Look for detached DOM nodes or retained closures. Check for non-cleaned subscriptions in useEffect, unremoved event listeners, and unreleased intervals/timers. Profile components with React DevTools.

By using the latest ref pattern:

const latestValue = useRef(value);
useEffect(() => {
  latestValue.current = value;
}, [value]);

Avoid depending on old values in closures by referring to latestValue.current inside callbacks.

Use differential bundling with tools like Vite/Rollup/Webpack + Babel. Serve modern ES modules (type="module") to new browsers and legacy bundles (with nomodule) to older ones.

Code splitting with dynamic import(), tree-shaking unused code, compressing assets (Gzip/Brotli), lazy loading non-critical routes/components, using HTTP/2 for parallel downloads.

Microtasks (Promise.then, queueMicrotask) run before rendering and have higher priority. Macrotasks (setTimeout, setInterval) run after. Improper microtask usage can block rendering.

When you want fine-grained control (e.g., making a property read-only, non-enumerable, or setting custom getter/setter logic), such as enforcing internal encapsulation.

JS might be storing session data in-memory (e.g., a token in a variable or non-persistent storage like sessionStorage). Upon tab refresh or inactivity, it gets lost. Use localStorage with expiration logic or rely on HTTP-only secure cookies.

Use a leading-edge debounce function (e.g., Lodash's _.debounce with { leading: true, trailing: false }).

Never inject raw user content into HTML. Use DOM APIs (textContent), sanitize inputs using libraries (e.g., DOMPurify), and enforce CSP headers on the server.

Service workers enable caching via the Cache API, intercept requests, and serve assets offline or from cache, improving performance and reliability in flaky networks.

this may not refer to the intended context. Solutions include using arrow functions (lexical this), bind(this) in constructors, or class fields in modern JS.

Use DevTools Performance tab: record runtime, look for long tasks, layout thrashing, reflows, expensive paint times, and JS call stacks. Use Lighthouse to audit performance.

Use React.memo to prevent unnecessary re-renders. Profile with React DevTools. Check useEffect dependencies. Avoid inline objects/arrays/functions passed as props.

Batch DOM changes using DocumentFragments or requestAnimationFrame. Avoid layout thrashing. Use transform and opacity for animations (they don’t trigger reflow).

Though ES6 specifies TCO, most modern JS engines (like V8/Chrome) don’t support it due to debugging complexities. TCO allows recursive functions to avoid growing the call stack.

It throws an error on circular references. Use a replacer function or libraries like flatted to handle them gracefully.

Use structured cloning (structuredClone(obj)) or libraries like Lodash _.cloneDeep. Avoid JSON.stringify for complex types (functions, Date, undefined).

Capture stack traces using window.onerror or ErrorBoundary (React). Use console.trace(), source maps, and runtime logs to trace user steps. Automate with Sentry or Bugsnag.

Storing metadata tied to DOM nodes or objects without preventing garbage collection (e.g., memoization caches, internal component state).

forEach doesn’t await async functions. Use for...of with await, or Promise.all() with map() for parallel processing.

Use IntersectionObserver API to detect when the sentinel element is visible and trigger more content loading.

Regularly take heap snapshots, monitor detached DOM nodes, use WeakMap where appropriate, and audit long-held event listeners.

Refactor using async/await, break into reusable async functions, or use Promises with chaining. Consider using RxJS for complex async flows.

Use Module Federation in Webpack 5 with shared singleton dependencies, or external CDNs with careful version pinning and semantic versioning.

It makes the object immutable at the top level. It’s shallow — nested objects can still be modified unless recursively frozen.

eval() executes arbitrary code, introducing XSS vulnerabilities, blocking JS optimizations, and potentially breaking scope chains.

Array.prototype.myFlat = function(depth = 1) {
  return this.reduce((acc, val) => 
    acc.concat(Array.isArray(val) && depth > 1 ? val.myFlat(depth - 1) : val), []);
};

Use ESLint rules (no-console) in shared config, CI integration to fail builds, and optionally strip logs using Babel plugins or Webpack production mode.

If closures capture variables or DOM nodes that are never released (e.g., through long-lived timers or event listeners), they prevent GC from cleaning up.

Avoid storing in localStorage/sessionStorage for critical secrets. Prefer secure HTTP-only cookies with same-site settings. Always validate on server.

requestAnimationFrame syncs with display refresh (usually 60fps), is more efficient and prevents layout jank. setTimeout is inaccurate due to delay drift.

It means JS is blocking the main thread for too long. Optimize long tasks, break large bundles, use code splitting, and reduce JS execution time.

The TDZ (Temporal Dead Zone) is the time between let/const declaration and initialization. Accessing variables in the TDZ throws a ReferenceError.

Used Proxy to intercept state changes in a data store for validation, auto-saving, or reactivity (like Vue does internally).

Use AbortController to cancel previous fetches. Track request IDs to ignore outdated responses. Use useEffect cleanup in React.

Use context API for shared global data. For more complex apps, use state libraries like Redux, Zustand, Jotai, or even RxJS.

The reconciler compares the virtual DOM trees and figures out the minimum number of DOM changes using keys and heuristics. It works with the renderer (e.g., DOM, native).

For controlled iteration (e.g., pagination), handling async flows via redux-saga, or simulating tasks like polling or streaming.

Use throttling (_.throttle) or track last click timestamp and disable the button temporarily.

A == comparison between 0 and '' evaluated true unexpectedly. Solved by always using === and applying lint rules.

Reduces readability and increases error propagation complexity. Use async/await, flatten control flow, or use error boundaries.

Used Web Workers for heavy data parsing or processing (e.g., CSV to JSON) to avoid UI blocking. Communicated via postMessage.

Part of Web Components. You can define reusable elements using customElements.define. Useful for shared UI across frameworks.

img.onload = () => console.log('Loaded!');
img.onerror = () => console.error('Failed!');

Module boundaries, shared utils, ESLint/Prettier, TypeScript, CI/CD, proper folder structure, API schemas (OpenAPI), performance budgets.

Use requestIdleCallback, or defer with setTimeout(fn, 0), or split bundles and load async via import().

Use Performance API (performance.getEntriesByType('script')) or monitor parse/compile timings via Lighthouse or Chrome DevTools.

Read DOM once, store values in variables, apply writes later. Avoid interleaving reads and writes.

Understand it’s placed in the macrotask queue. The actual execution waits for the call stack to clear and other tasks to finish.

Include API version in URL (/api/v2/...), or use custom headers. Abstract logic in a version-aware API client.