Master these 31 carefully curated interview questions to ace your next Javascript interview.
JavaScript has 8 data types: String, Number, BigInt, Boolean, Undefined, Null, Symbol, and Object.
Primitive types (String, Number, BigInt, Boolean, Undefined, Null, Symbol) are immutable and stored by value. Object is reference type and includes arrays, functions, dates, and regular expressions. typeof operator can check types, though typeof null returns 'object' (a known bug). ES6 added Symbol for unique identifiers and BigInt for arbitrarily large integers.
var is function-scoped and hoisted; let is block-scoped and not hoisted; const is block-scoped and cannot be reassigned.
var declarations are hoisted to the top of their function scope and initialized as undefined. let and const are block-scoped (within {}) and exist in a 'temporal dead zone' until declared. const requires initialization at declaration and prevents reassignment, but object/array contents can still be mutated. Best practice: use const by default, let when reassignment is needed, avoid var.
Hoisting is JavaScript's behavior of moving declarations to the top of their scope before code execution.
During compilation, JavaScript moves variable and function declarations to the top of their containing scope. Function declarations are fully hoisted (can be called before declaration). var variables are hoisted but initialized as undefined. let/const are hoisted but not initialized, creating a temporal dead zone. Class declarations are not hoisted. This is why you can call a function before its declaration in code.
== performs type coercion before comparison; === compares both value and type without coercion.
The == operator converts operands to the same type before comparing (e.g., '5' == 5 is true). The === operator checks both value and type strictly (e.g., '5' === 5 is false). Always prefer === to avoid unexpected coercion bugs. Notable: null == undefined is true, NaN !== NaN, and objects are compared by reference, not value.
A closure is a function that retains access to its outer scope's variables even after the outer function has returned.
When a function is created inside another function, it forms a closure over the outer function's variables. The inner function 'closes over' and remembers the environment in which it was created. This enables data privacy (module pattern), factory functions, and callback patterns. Example: function counter() { let count = 0; return () => ++count; } — the returned function remembers count.
undefined means a variable has been declared but not assigned; null is an intentional assignment representing 'no value'.
undefined is the default value of uninitialized variables, missing function parameters, and non-existent object properties. null is explicitly assigned to indicate 'no value' or 'empty'. typeof undefined is 'undefined'; typeof null is 'object' (a legacy bug). null == undefined is true (loose), but null === undefined is false (strict). Use null for intentional absence of value.
The event loop is the mechanism that handles asynchronous callbacks by monitoring the call stack and task queues.
JavaScript is single-threaded. The event loop continuously checks: (1) Is the call stack empty? (2) If yes, take the first callback from the microtask queue (Promises, queueMicrotask). (3) If microtask queue is empty, take from the macrotask queue (setTimeout, setInterval, I/O). Microtasks always execute before macrotasks. This is why Promise.then() runs before setTimeout(fn, 0). The browser also handles rendering between macrotasks.
call invokes with arguments listed; apply invokes with arguments as array; bind returns a new function with bound this.
func.call(thisArg, arg1, arg2) — immediately invokes with given this and individual arguments. func.apply(thisArg, [args]) — same but accepts arguments as an array. func.bind(thisArg, arg1) — returns a new function with this permanently bound (does not invoke immediately). Use call/apply for borrowing methods; bind for event handlers and callbacks where you need to preserve context.
Promises represent a value that may be available now, later, or never. They have three states: pending, fulfilled, or rejected.
A Promise is created with new Promise((resolve, reject) => {}). It starts as 'pending', then transitions to 'fulfilled' (resolve called) or 'rejected' (reject called). Chain .then() for success, .catch() for errors, .finally() for cleanup. Promise.all() waits for all to resolve; Promise.race() resolves with the first settled; Promise.allSettled() waits for all regardless of outcome. Async/await is syntactic sugar over Promises.
Objects can inherit properties from other objects through the prototype chain, where each object has an internal [[Prototype]] link.
Every JavaScript object has a hidden [[Prototype]] property linking to another object. When accessing a property, JavaScript first checks the object itself, then walks up the prototype chain. Object.create(proto) creates an object with proto as its prototype. Constructor functions set prototypes via .prototype property. ES6 classes are syntactic sugar over prototypal inheritance. The chain ends at Object.prototype, whose prototype is null.
Shallow copy copies only the top-level properties; deep copy recursively copies all nested objects.
Shallow copy methods: Object.assign(), spread operator {...obj}, Array.from(). These copy references for nested objects, so mutations in nested objects affect both copies. Deep copy methods: structuredClone() (modern), JSON.parse(JSON.stringify(obj)) (loses functions/dates), or libraries like lodash.cloneDeep(). structuredClone() handles circular references and most types but not functions or DOM nodes.
Generators are functions that can pause and resume execution, yielding multiple values over time.
Declared with function* syntax. Calling a generator returns an iterator. yield pauses execution and returns a value. Calling .next() resumes from where it paused. yield* delegates to another generator. Generators enable lazy evaluation, infinite sequences, and are the foundation of async/await (internally). They maintain their own execution context between yields.
CommonJS uses require/module.exports (synchronous, Node.js); ES Modules use import/export (static, async, browser-native).
CommonJS: require() is synchronous, modules are cached after first load, module.exports defines the export. ES Modules: import/export are statically analyzable (tree-shaking), loaded asynchronously, use strict mode by default, have live bindings (exports update automatically). Node.js supports both via .mjs extension or 'type: module' in package.json. ESM is the standard going forward.
Proxy wraps an object to intercept and customize fundamental operations like property access, assignment, and function calls.
new Proxy(target, handler) creates a proxy. The handler object defines 'traps' like get, set, has, deleteProperty, apply, construct. Used for validation (reject invalid values), logging/debugging, default values, data binding in frameworks (Vue 3 reactivity), and creating observable objects. Reflect API provides default behavior for each trap. Proxies are transparent — typeof proxy === typeof target.
JavaScript uses mark-and-sweep garbage collection, automatically freeing memory for objects that are no longer reachable.
The GC starts from 'roots' (global object, current call stack) and marks all reachable objects. Unreachable objects are collected. V8 uses generational GC: young generation (Scavenger, fast) and old generation (Mark-Sweep-Compact). Memory leaks occur from: forgotten timers/callbacks, closures retaining references, detached DOM nodes, and global variables. WeakMap/WeakSet allow values to be garbage collected when keys are no longer referenced.
WeakMap and WeakSet hold weak references to objects, allowing garbage collection when no other references exist.
WeakMap: keys must be objects, values can be anything. Keys are weakly referenced — if no other reference to the key exists, it's garbage collected along with its value. Not iterable, no size property. WeakSet: only stores objects, also weakly referenced. Use cases: private data storage, caching DOM node metadata, tracking object instances without preventing GC. Unlike Map/Set, they don't prevent memory leaks.
Web Workers run JavaScript in background threads, enabling parallel execution without blocking the main thread.
Workers run in separate threads with their own event loop and global scope (no DOM access). Communication via postMessage/onmessage (structured clone). Types: Dedicated Workers (one-to-one), Shared Workers (shared across tabs), Service Workers (proxy for network requests, offline support). Use for CPU-intensive tasks: image processing, data parsing, complex calculations. transferable objects avoid copying overhead.
Tree shaking is a dead-code elimination technique that removes unused exports from the final bundle.
Works with ES Modules because import/export are statically analyzable at build time. Bundlers (Webpack, Rollup, Vite) analyze the dependency graph, trace which exports are actually imported, and eliminate the rest. Requires: ES Module syntax, sideEffect-free code (or 'sideEffects: false' in package.json), production mode. CommonJS require() cannot be tree-shaken because it's dynamic. Write pure, side-effect-free modules for best results.
Use Chrome DevTools Memory tab to take heap snapshots, compare allocations, and identify retained objects.
Steps: (1) Open DevTools > Memory tab. (2) Take a heap snapshot, perform the leaking action, take another snapshot. (3) Compare snapshots to find growing objects. (4) Check 'Retainers' to see what's keeping objects alive. (5) Common causes: event listeners not removed, setInterval not cleared, closures capturing large scopes, detached DOM trees, global caches growing unbounded. (6) Fix by removing listeners in cleanup, using WeakRef/WeakMap, and clearing timers.
Code splitting, lazy loading, tree shaking, minification, compression, and critical rendering path optimization.
Strategies: (1) Code splitting with dynamic import() to load routes/features on demand. (2) Tree shaking to remove unused code. (3) Minify and compress (gzip/brotli). (4) Defer non-critical scripts. (5) Preload critical resources. (6) Use CDN for static assets. (7) Optimize images (WebP, lazy loading). (8) Reduce third-party scripts. (9) Use service workers for caching. (10) Analyze with Lighthouse and webpack-bundle-analyzer.
Move the heavy computation to a Web Worker or break it into chunks using requestIdleCallback/setTimeout.
The main thread is blocked because synchronous computation prevents the event loop from processing UI updates. Solutions: (1) Web Worker for true parallel processing. (2) Chunk processing with setTimeout(processChunk, 0) to yield to the browser between chunks. (3) requestIdleCallback for non-urgent work. (4) Virtual scrolling for large lists (only render visible items). (5) Pagination/infinite scroll to limit data. (6) Use streaming/incremental processing instead of loading everything at once.
The output is 'string'. typeof 1 returns 'number' (a string), and typeof 'number' returns 'string'.
Step by step: typeof 1 evaluates to the string 'number'. Then typeof 'number' evaluates to 'string' because 'number' is a string value. The typeof operator always returns a string. This is a common trick question testing understanding of the typeof operator's return type.
Debounce delays function execution until after a specified wait time has elapsed since the last call.
Implementation: function debounce(fn, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } — Each call resets the timer. The function only executes after the user stops invoking it for 'delay' milliseconds. Used for search input, window resize, scroll handlers. Throttle is different — it guarantees execution at regular intervals.
React maintains a lightweight JS representation of the DOM, diffs changes between renders, and applies minimal DOM updates.
Process: (1) Component renders JSX which creates a virtual DOM tree (plain JS objects). (2) On state change, React creates a new virtual DOM tree. (3) React's reconciliation algorithm (Fiber) diffs the old and new trees. (4) It calculates the minimum set of DOM operations needed. (5) Batches and applies changes to the real DOM. (6) Fiber architecture enables prioritized, interruptible rendering. Keys help React identify which items changed in lists.
eval() executes arbitrary code, creating XSS vulnerabilities, scope pollution, and performance issues.
Risks: (1) Code injection — user input in eval() enables XSS attacks. (2) Scope access — eval can read/modify local variables. (3) Performance — prevents engine optimizations, no JIT compilation. (4) Debugging difficulty — eval'd code doesn't appear in stack traces properly. Alternatives: JSON.parse() for data, Function constructor for dynamic functions (still risky), template literals for string building, Map objects for dynamic property access.
Temporal is a modern date/time API replacing the flawed Date object, offering immutable, timezone-aware date handling.
The Date object has known issues: mutable, zero-indexed months, no timezone support, inconsistent parsing. Temporal provides: Temporal.PlainDate (date without time), Temporal.PlainTime, Temporal.ZonedDateTime (with timezone), Temporal.Duration, Temporal.Instant (exact point in time). All values are immutable. Arithmetic is built-in: date.add({ days: 5 }). Currently at Stage 3 in TC39. Polyfill available via @js-temporal/polyfill.
structuredClone() creates deep copies of objects, handling circular references and most built-in types unlike JSON methods.
structuredClone(value) performs a deep copy using the structured clone algorithm. Supports: Arrays, Objects, Maps, Sets, Dates, RegExp, Blob, File, ArrayBuffer, ImageData, circular references. Does NOT support: Functions, DOM nodes, Symbols, property descriptors (getters/setters). Advantages over JSON.parse(JSON.stringify()): handles circular refs, preserves Date/Map/Set types, supports binary data. Available in all modern browsers and Node.js 17+.
Use IntersectionObserver to detect when a sentinel element enters the viewport, then fetch and append new data.
Steps: (1) Render initial data. (2) Place a sentinel div at the bottom. (3) Create IntersectionObserver watching the sentinel. (4) When sentinel is visible, fetch next page. (5) Use virtual scrolling (react-window/react-virtualized) for lists with 1000+ items — only renders visible items. (6) Implement loading states and error handling. (7) Consider cursor-based pagination over offset for consistency. (8) Debounce rapid scroll events. (9) Add 'scroll to top' button for UX.
Use a cache-first strategy with Map/WeakMap, stale-while-revalidate pattern, and TTL-based expiration.
Implementation: (1) Create a cache Map with key = URL + params, value = { data, timestamp, ttl }. (2) On request, check cache first. If valid (not expired), return cached data. (3) If stale, return cached data AND fetch fresh data in background (stale-while-revalidate). (4) Use AbortController to cancel duplicate in-flight requests. (5) Set max cache size to prevent memory issues. (6) Clear cache on user logout. Libraries like SWR and React Query implement this pattern with additional features like deduplication and prefetching.
Use AbortController to cancel stale requests, mutex locks, request deduplication, and optimistic UI with rollback.
Race conditions occur when multiple async operations compete. Solutions: (1) AbortController: cancel previous fetch when new one starts. (2) Sequence ID: track latest request, ignore responses from older requests. (3) Mutex/semaphore: ensure only one operation runs at a time using a lock. (4) Debounce: delay execution until user stops typing. (5) Optimistic updates with rollback on failure. (6) Server-side idempotency keys for payment/mutation operations. (7) React 18's useTransition for UI state race conditions.
Ready to master Javascript?
Start learning with our comprehensive course and practice these questions.