Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
JavaScript Interview Questions (Fundamentals)
A comprehensive guide to JavaScript interview questions, organized by difficulty level. This collection covers fundamental concepts to advanced topics commonly asked in web development interviews. Every question includes what the interviewer is really testing, detailed answers with real-world context, red flag responses, and follow-up chains that mirror how actual interviews probe for depth.Easy Level Questions
1. What is JavaScript and where is it commonly used?
1. What is JavaScript and where is it commonly used?
- Single-threaded with an event loop: Unlike Java or C++, JS runs on one thread but uses an event-driven, non-blocking I/O model. This is the architectural foundation that makes Node.js capable of handling 10,000+ concurrent connections on a single process — something that a thread-per-request model like traditional Java servlets struggles with at scale.
- Interpreted and JIT-compiled: Modern engines like V8 (Chrome/Node.js), SpiderMonkey (Firefox), and JavaScriptCore (Safari) use Just-In-Time compilation. V8 compiles JavaScript directly to native machine code before executing it, which is why Node.js can compete with compiled languages for I/O-heavy workloads.
- Prototype-based inheritance: Unlike classical OOP languages, JavaScript uses prototypal inheritance. Every object has a hidden
[[Prototype]]link. This is fundamental to understanding how the language works under the hood.
- Frontend web development: React (Meta), Angular (Google), Vue — these frameworks power apps serving billions of users. Netflix’s entire TV UI runs on React.
- Backend development: Node.js powers PayPal’s backend (which saw a 35% decrease in average response time after migrating from Java), LinkedIn’s mobile backend, and Walmart’s server-side rendering.
- Mobile apps: React Native (Instagram, Discord, Shopify), Ionic, NativeScript.
- Desktop apps: Electron powers VS Code, Slack, and Discord desktop clients.
- Serverless/Edge computing: Cloudflare Workers, AWS Lambda, Vercel Edge Functions — all run JavaScript at the edge with sub-millisecond cold starts.
- IoT and embedded: Johnny-Five framework for Arduino, Espruino for microcontrollers.
fetch() or setTimeout(), the actual I/O work is delegated to the OS kernel or a thread pool (libuv maintains a default pool of 4 threads in Node.js). The main thread is free to process other code. When the I/O completes, a callback is placed in the task queue, and the event loop picks it up when the call stack is empty. This is why Node.js can handle 50K concurrent WebSocket connections on a single process — it’s not creating 50K threads, it’s multiplexing I/O events on one thread.Q: If JavaScript is single-threaded, what are Web Workers and how do they fit in?Web Workers provide true multi-threading in browsers. A Worker runs in a separate OS thread with its own global scope — it cannot access the DOM or share memory directly with the main thread (they communicate via postMessage()). This is by design to avoid race conditions. In Node.js, the equivalent is worker_threads. A practical use case: Figma uses Web Workers extensively to run their design engine computations off the main thread so the UI stays responsive at 60fps. The key constraint is that communication between workers uses structured cloning (serialization), so passing large objects has overhead. SharedArrayBuffer exists for shared memory but requires careful synchronization with Atomics.Q: How does JavaScript compare to TypeScript, and when would you choose one over the other?TypeScript is a strict superset of JavaScript that adds static type checking at compile time. Every valid JS file is valid TS, but not vice versa. In practice, you’d choose TypeScript for any project with more than 2-3 developers or any codebase expected to live longer than 6 months. The type system catches entire categories of bugs at compile time — Airbnb reported that 38% of bugs they studied could have been prevented by TypeScript. The tradeoff is slightly slower development velocity on small prototypes and a learning curve for the type system’s advanced features (conditional types, mapped types, template literal types). For a weekend hackathon, plain JS is fine. For a production API serving millions of requests, TypeScript is practically non-negotiable in the modern ecosystem.2. What are template literals in JavaScript?
2. What are template literals in JavaScript?
` instead of quotes and support string interpolation with ${expression} syntax, multi-line strings, and tagged templates for advanced string processing.Basic string interpolation:${} — anything that evaluates:- styled-components:
styled.div`color: ${props => props.primary ? 'blue' : 'gray'};`— generates CSS-in-JS - GraphQL:
gql`query { users { name } }`— parses GraphQL queries at build time - SQL injection prevention: Libraries like
sqluse tagged templates to auto-parameterize queries:sql`SELECT * FROM users WHERE id = ${userId}`becomes a parameterized query, not a raw string
safeHTML tagged template would receive strings = ['<div>', '</div>'] and values = [userInput] separately, allowing you to HTML-encode userInput before combining. Libraries like lit-html do exactly this for safe DOM rendering. For SQL, slonik and sql-template-tag use this pattern to ensure every ${variable} is treated as a parameterized value, never as raw SQL. It’s an architectural pattern that makes the secure path the default path.Q: What happens if you nest template literals?It works perfectly. Since everything inside ${} is an expression, and a template literal is an expression, you can nest them: `Hello ${`world ${name}`}` evaluates to "Hello world Alice". This is commonly used in conditional rendering patterns in React JSX alternatives or when building complex strings conditionally. However, deep nesting hurts readability — if you’re nesting more than one level, extract to a variable or function.Q: How do template literals affect performance compared to string concatenation?In modern engines (V8, SpiderMonkey), the performance difference is negligible for typical use cases — both are optimized heavily. V8 internally optimizes template literals similarly to concatenation. Where you see a real difference is in tagged templates with complex processing logic, since the tag function runs on every evaluation. In hot loops processing millions of iterations, benchmark first. But for 99% of real-world code, readability wins over micro-optimization. The bigger performance concern is creating strings in hot paths at all versus using string builders or array joins for very large string assembly.3. What is hoisting? Give an example.
3. What is hoisting? Give an example.
- Creation phase: The engine scans the code and allocates memory for all declarations.
varvariables getundefined,let/constget marked as “uninitialized,” and function declarations get the entire function body. - Execution phase: Code runs line by line, assigning values and executing statements.
let/const declaration is reached. This is intentional — it prevents you from accidentally using a variable before it’s been assigned a meaningful value.Function expressions are NOT fully hoisted:let/const are technically hoisted too (just not initialized).Follow-up questions:Q: If let and const are also hoisted, why do people say “only var is hoisted”?This is one of the most common misconceptions. All declarations (var, let, const, function, class) are hoisted. The difference is initialization. var is hoisted AND initialized to undefined. let/const are hoisted but left uninitialized — accessing them before declaration throws a ReferenceError because they’re in the Temporal Dead Zone. People say “only var is hoisted” because var’s hoisting has visible effects (returns undefined instead of throwing), while let/const’s hoisting is only observable through the TDZ error message (it says “Cannot access before initialization” rather than “is not defined” — proving the engine knows the variable exists).Q: How does hoisting interact with ES modules?ES modules are always in strict mode and have their own scope. Imports are hoisted and are read-only live bindings — they’re available throughout the module even before the import statement lexically. However, the imported values might not be initialized yet if there are circular dependencies. This is why circular imports can give you undefined for a value that’s later assigned. Node.js CommonJS (require) doesn’t hoist — it executes synchronously at the point of the call. This behavioral difference is a common source of bugs when migrating from CJS to ESM.Q: Can you describe a real production bug caused by hoisting?Classic scenario: a developer declares var config at the top of a module and a var config inside a callback. Because var is function-scoped (not block-scoped), the inner var config doesn’t shadow — it’s the same variable. In an async callback, the outer config gets overwritten unexpectedly, causing the entire module to use wrong configuration. At a company I know of, this exact pattern caused a payment processing module to use test API keys in production for 47 minutes because a var in a conditional block leaked into the module scope. The fix was migrating to const/let. This is why the industry standard is to ban var entirely via ESLint rules (no-var).4. Explain the difference between var, let, and const
4. Explain the difference between var, let, and const
const, and whether you’ve internalized modern JS best practices in real codebases.Answer:| Feature | var | let | const |
|---|---|---|---|
| Scope | Function scope | Block scope | Block scope |
| Reassignment | Yes | Yes | No |
| Re-declaration | Yes (silently) | No (SyntaxError) | No (SyntaxError) |
| Hoisting behavior | Hoisted, initialized to undefined | Hoisted, TDZ until declaration | Hoisted, TDZ until declaration |
| Global object property | Yes (var x creates window.x) | No | No |
const prevents reassignment, NOT mutation:var and the global object — a real security concern:window.apiKey. This is an actual attack vector.Best practices enforced in production codebases:- Use
constby default — it communicates intent (“this binding won’t change”) and prevents accidental reassignment. About 90%+ of variables in well-written code should beconst. - Use
letwhen you genuinely need to reassign — loop counters, accumulators, state that changes. - Never use
var— configure ESLint withno-varrule. There’s no modern use case wherevaris preferable. - Most major style guides (Airbnb, Google, StandardJS) enforce
const>let>var.
var creates properties on the global object.Follow-up questions:Q: If const doesn’t make objects immutable, how do you achieve true immutability in JavaScript?There are several layers. Object.freeze() makes an object’s own properties non-writable and non-configurable, but it’s shallow — nested objects are still mutable. For deep freeze, you’d need a recursive function or a library like Immer (used by Redux Toolkit). In practice, most teams use Immer’s produce() function which gives you an immutable update pattern with mutable-looking syntax. TypeScript’s readonly modifier provides compile-time immutability but has no runtime effect. For truly immutable data structures with structural sharing (efficient memory use), libraries like Immutable.js provide persistent data structures — but the ecosystem has largely moved toward Immer because it works with plain JS objects.Q: Why does let inside a for loop create a new binding per iteration while var doesn’t?This is specified in the ECMAScript standard (Section 14.7.4.2). When the engine encounters for (let i = ...), it creates a new lexical environment for each iteration and copies the current value of i into it. With var, there’s only one variable shared across all iterations because var is function-scoped. This is why the classic setTimeout in a for loop puzzle exists — with var, all closures share the same i (which ends up at its final value), while with let, each closure captures its own i. Before let existed, the workaround was wrapping in an IIFE: (function(j) { setTimeout(() => console.log(j), 100); })(i);Q: Are there any edge cases where var actually behaves differently from let in a way that matters beyond scope?Yes. One surprising case is the switch statement: all case clauses share a single block scope, so let x = 1 in case A and let x = 2 in case B causes a SyntaxError (duplicate declaration in same scope). With var, both work fine since they’re the same function-scoped variable. The fix is wrapping each case in braces: case A: { let x = 1; ... }. Another edge case: var declarations in catch blocks leak out (catch(e) { var leaked = true; } — leaked is accessible outside), while let stays contained. In eval(), var creates variables in the calling scope, while let stays within the eval scope.5. What are the data types available in JavaScript?
5. What are the data types available in JavaScript?
typeof null === 'object').Answer:JavaScript has 7 primitive types and 1 structural type (Object). Everything else (Arrays, Functions, Dates, RegExp, Map, Set) is an Object underneath.Primitive Data Types (immutable, passed by value):- Number: 64-bit IEEE 754 floating-point. There’s no separate integer type.
0.1 + 0.2 problem has caused real production bugs in financial calculations. Stripe, Shopify, and every payment system uses integer cents (e.g., $10.50 = 1050) to avoid floating-point errors.- String: UTF-16 encoded sequence of characters. Strings are immutable.
-
Boolean:
trueorfalse. Understand falsy values:false,0,-0,'',null,undefined,NaN,0n. Everything else is truthy (including[],{},'false','0'). - Undefined: Automatically assigned to declared-but-unassigned variables, missing function parameters, and missing object properties.
-
Null: Intentional absence of value.
typeof null === 'object'is a famous bug from JavaScript’s first implementation in 1995 — values were tagged with type bits, and null’s tag matched object’s tag. It was never fixed for backward compatibility. - Symbol (ES6): Guaranteed unique identifier, primarily used as object property keys to avoid name collisions.
- BigInt (ES2020): Arbitrary-precision integers for values beyond
Number.MAX_SAFE_INTEGER(2^53 - 1 = 9,007,199,254,740,991).
length property. Another red flag: not knowing about the 0.1 + 0.2 problem — this comes up in every financial or e-commerce codebase.Follow-up questions:Q: How do you reliably check if something is an array, since typeof [] returns 'object'?Use Array.isArray(value) — it’s the only reliable way. typeof returns 'object' for arrays. instanceof Array fails across different realms (iframes, different global contexts) because each realm has its own Array constructor. Array.isArray was specifically created to solve this cross-realm problem. Under the hood, it checks the internal [[Class]] slot of the object.Q: Explain how JavaScript’s type coercion works and give an example of a production bug it could cause.JavaScript coerces types implicitly in many contexts. The rules follow the Abstract Equality Comparison Algorithm (Section 7.2.14 of the spec). For example, '5' == 5 is true because the string is coerced to a number. The dangerous real-world scenario: a form input returns the string '0', and you check if (quantity) — this is truthy (non-empty string), even though the intent was to check for a non-zero quantity. The fix: explicit comparison if (quantity !== '0' && quantity !== '') or better yet, parse it: if (Number(quantity) > 0). Another classic: [] == false is true, but if ([]) is truthy. The coercion paths are different — == triggers ToPrimitive, while if() calls ToBoolean. This is why === is the default in every major linter configuration.Q: When would you actually use Symbol in production code?Three main scenarios: (1) Library/framework internal properties that shouldn’t conflict with user code — React uses Symbol.for('react.element') to tag React elements. (2) Implementing iteration protocols — making custom objects iterable by implementing Symbol.iterator. (3) Defining custom behavior for operators via well-known symbols like Symbol.toPrimitive (controls type coercion), Symbol.hasInstance (controls instanceof), and Symbol.species (controls which constructor is used for derived objects). In day-to-day app code, you rarely create Symbols directly, but you use them implicitly every time you write a for...of loop.6. What is an array in JavaScript and how do you access its elements?
6. What is an array in JavaScript and how do you access its elements?
length property, and methods inherited from Array.prototype. Unlike arrays in C or Java, JavaScript arrays are dynamically sized and can hold mixed types (though this is rarely desirable).Creating arrays:for...in and for...of for arrays, and why does it matter?for...of iterates over values using the iterator protocol (calls Symbol.iterator). for...in iterates over enumerable property keys, including inherited ones and non-integer properties. For arrays, for...in can yield unexpected results: if someone adds Array.prototype.customMethod = ..., for...in will include 'customMethod' in iteration. It also returns string keys, not numbers: '0', '1', not 0, 1. The rule: use for...of for arrays (and any iterable), for...in for plain objects (and even then, pair it with hasOwnProperty).Q: How would you efficiently remove duplicates from an array?The cleanest modern approach: [...new Set(array)] — this works for primitives and runs in O(n) time. For objects, you need a custom approach since Set compares by reference, not by value. Common patterns: array.filter((item, index, self) => self.findIndex(t => t.id === item.id) === index) — this is O(n^2) though. For performance, use a Map: create a Map keyed by the unique property, then take the values. In production, if deduplication is a frequent operation on large datasets, consider keeping data in a Map or Set from the start rather than converting back and forth.Q: What are typed arrays and when would you use them over regular arrays?Typed arrays (Int8Array, Uint32Array, Float64Array, etc.) provide raw binary data buffers with fixed types and sizes. They’re backed by ArrayBuffer and are essential for WebGL/GPU computing, Web Audio API, binary protocol parsing (WebSocket binary frames), image/video processing (Canvas getImageData returns a Uint8ClampedArray), and WebAssembly interop. They’re much more memory-efficient than regular arrays for numeric data — a Float64Array(1000) uses exactly 8KB, while a regular array of 1000 numbers uses significantly more due to object overhead per element. Typed arrays don’t have most Array methods (push, map returns typed array, no splice), so they’re specialized tools, not general-purpose replacements.7. Explain the difference between == and === in JavaScript
7. Explain the difference between == and === in JavaScript
=== is the default in production code.Answer:=== (strict equality) compares both type and value with no coercion. == (loose equality) performs type coercion before comparing, following a complex algorithm defined in the ECMAScript spec (Section 7.2.14).| Operator | Name | Type Coercion | Example |
|---|---|---|---|
== | Abstract equality | Yes | '5' == 5 is true |
=== | Strict equality | No | '5' === 5 is false |
[] == ![] is true (the ultimate interview trick):Object.is() — the most precise comparison:=== and !==. The only acceptable == use case is value == null which conveniently checks for both null and undefined in one expression. ESLint’s eqeqeq rule enforces this.Red flag answer: “Double equals checks value, triple equals checks value and type.” This is a surface-level description that doesn’t demonstrate understanding of the coercion algorithm. Candidates who can’t explain WHY '' == false is true or who’ve never heard of Object.is() are missing practical depth.Follow-up questions:Q: Why is NaN !== NaN in JavaScript, and how does this affect real code?NaN (Not-a-Number) is defined by IEEE 754 to be not equal to anything, including itself. This is a deliberate standard decision — NaN represents an undefined or unrepresentable computation result, and comparing two undefined results shouldn’t be considered equal. In practice, this means you can’t do if (result === NaN) to check for NaN. Use Number.isNaN(value) (not the global isNaN() which coerces first — isNaN('hello') returns true because it coerces to Number('hello') which is NaN). A real bug: parsing user input with parseFloat('abc') returns NaN, and checking parsedValue === NaN is always false, so the invalid input silently passes through validation. Always use Number.isNaN.Q: When, if ever, would you deliberately use == instead of ===?The only widely accepted case: value == null which is equivalent to value === null || value === undefined. This is a common pattern to check if a value is “empty” (null or undefined) in one concise expression. Some coding standards (notably the eslint-config-standard preset) allow this single exception. Libraries like Lodash use this pattern extensively. Beyond this, always use ===. Some legacy codebases use == for string-number comparison in form handling (if (input == 42)), but the modern approach is to explicitly parse: if (Number(input) === 42).Q: How does React use equality comparison internally, and why does it matter for rendering?React uses Object.is() (not ===) to compare state values in hooks like useState and useReducer. The practical difference: Object.is(0, -0) is false while 0 === -0 is true. For state updates, if Object.is(oldState, newState) returns true, React skips the re-render. This is why setState(prevState) with the same reference doesn’t re-render, but setState({...prevState}) always re-renders (new object reference). For useMemo and useCallback dependency arrays, React compares each dependency with Object.is. Understanding this prevents unnecessary re-renders — a common performance issue in React apps where developers create new object/array references on every render without realizing it.8. What is the purpose of the isNaN() function?
8. What is the purpose of the isNaN() function?
isNaN() and Number.isNaN(), and that the global version has a coercion bug that has caused real production issues.Answer:isNaN() checks if a value is NaN, but there are two versions with critically different behavior:Global isNaN() — the broken version:isNaN('hello') returns true, but 'hello' is NOT NaN — it’s a string. The global function answers: “Would this value be NaN if I tried to make it a number?” That’s usually not what you want.Number.isNaN() — the correct version (ES6):isNaN() without mentioning Number.isNaN(), or not knowing that typeof NaN === 'number'. In code reviews, using the global isNaN() is a smell because it hides coercion bugs.Follow-up questions:Q: How would you safely validate that a user input from a form is a valid number?Form inputs are always strings. The robust approach: first check for empty/whitespace (value.trim() === ''), then use Number(value) for conversion (not parseInt which stops at the first non-numeric character — parseInt('42abc') returns 42!), then check Number.isFinite(result) (which rejects NaN, Infinity, and -Infinity). For specific formats, use a regex first. For currency, strip currency symbols and commas before parsing, and use integer cents to avoid floating-point issues. In React, libraries like Zod or Yup handle this with schemas: z.coerce.number().positive().finite().Q: What’s the difference between Number(), parseInt(), and parseFloat() for string-to-number conversion?Number(value) converts the entire string or returns NaN if any part is invalid: Number('42px') is NaN. parseInt(string, radix) parses from the beginning and stops at the first non-numeric character: parseInt('42px') is 42, parseInt('0xFF', 16) is 255. Always pass the radix (second argument) — without it, parseInt('08') historically returned 0 in some engines (octal interpretation). parseFloat(string) is similar but handles decimals: parseFloat('3.14meters') is 3.14. In production, Number() is safest because it’s strictest. Use parseInt/parseFloat only when you deliberately want to extract a number from a string with trailing non-numeric content.9. What is null and undefined?
9. What is null and undefined?
typeof null bug, and how these values interact with APIs, default parameters, and optional chaining.Answer:Both represent “absence of value” but with different semantics and use cases:| Feature | undefined | null |
|---|---|---|
| Meaning | System-assigned: “no value yet” | Developer-assigned: “intentionally empty” |
typeof | 'undefined' | 'object' (historical bug since 1995) |
| In JSON | Not valid (omitted from serialization) | Valid JSON value |
| Default params | Triggers default | Does NOT trigger default |
| Numeric coercion | NaN | 0 |
undefined appears automatically:null intentionally:<Component title={null} /> won’t use the default prop value, while <Component /> (where title is undefined) will.Optional chaining and nullish coalescing (modern patterns):ReferenceError. Also: not knowing typeof null === 'object' or the default parameter behavior difference.Follow-up questions:Q: Why does typeof null return 'object' and will it ever be fixed?In the original JavaScript implementation (Brendan Eich, 1995, 10 days), values were stored as a type tag + value. Objects had type tag 0, and null was represented as the null pointer (0x00), which also had a 0 type tag. So typeof null checks the type tag, sees 0, and returns 'object'. A fix was proposed for ES6 (typeof null === 'null') but was rejected because it would break too much existing code on the web. It will never be fixed. In practice, to check for null specifically, use value === null.Q: How does the optional chaining operator handle method calls and bracket notation?Optional chaining works in three forms: obj?.prop (property access), obj?.[expr] (computed property), and obj?.method() (method call). For method calls, obj?.method() will return undefined if obj is null/undefined OR if method doesn’t exist on obj. The entire chain short-circuits: a?.b.c.d — if a is null, it returns undefined immediately without evaluating .b.c.d. A subtle gotcha: obj?.method() will still throw if method exists but isn’t a function. It only guards against null/undefined, not against the property being the wrong type.Q: In API design, when should a field be null vs. omitted (undefined) vs. an empty value?This is an important API contract question. Convention: null means “this field exists in the schema but has no value for this record” (e.g., middleName: null). Omitted/undefined means “this field wasn’t requested or doesn’t apply” (e.g., not including address when it wasn’t in the query’s select clause). Empty string "" means “this field has a value and it’s an empty string” (different from null). In GraphQL, this distinction is explicit: fields can be nullable, and you can query specific fields. In REST APIs, use null for “intentionally empty” and omit the field for “not applicable.” JSON Merge Patch (RFC 7396) uses null specifically to mean “delete this field” — so the semantic difference has protocol-level implications.10. Explain the use of the typeof operator
10. Explain the use of the typeof operator
typeof’s quirks (null returns ‘object’, functions return ‘function’, arrays return ‘object’), and what alternatives exist for more precise type checking.Answer:typeof is a unary operator that returns a string indicating the type of the operand. It’s fast, safe (doesn’t throw on undeclared variables), but has several well-known limitations.The complete typeof table:typeof — safe check on undeclared variables:typeof to check for arrays (typeof arr === 'array' — this doesn’t work, returns 'object'). Or not knowing that typeof null returns 'object'. Also, using typeof when instanceof or Array.isArray would be more appropriate.Follow-up questions:Q: Why does typeof return 'function' for functions but 'object' for everything else that’s an object?Functions are objects in JavaScript (they have properties, can be assigned to variables, etc.), but typeof treats them specially because functions are “callable objects.” The spec defines a [[Call]] internal method on function objects, and typeof checks for this. This special-casing was a practical design choice — checking if something is callable is one of the most common type checks in JavaScript. Interestingly, typeof class Foo {} also returns 'function' because classes are syntactic sugar over constructor functions.Q: How would you build a robust type-checking utility for a production library?The gold standard is Object.prototype.toString.call(value) which returns [object Type] for all built-in types. Wrap it: const getType = v => Object.prototype.toString.call(v).slice(8, -1). This correctly distinguishes Array, Null, Date, RegExp, Map, Set, etc. For custom classes, you can override this by implementing Symbol.toStringTag. Libraries like Lodash provide isPlainObject, isArray, isFunction etc. that handle edge cases including cross-realm checks. In TypeScript, you’d pair runtime checks with type guards: function isString(value: unknown): value is string — giving you runtime safety and compile-time narrowing.Q: What’s the difference between typeof and instanceof, and when does instanceof fail?typeof checks the value’s type tag (a primitive operation). instanceof checks the prototype chain — it asks “does Constructor.prototype appear anywhere in the object’s prototype chain?” instanceof fails across realms: if you pass an array from an iframe to the parent window, arr instanceof Array is false because the iframe has its own Array constructor. Symbol.hasInstance lets you customize instanceof behavior. Also, instanceof doesn’t work with primitives: 'hello' instanceof String is false (it’s a primitive, not a String object). Use typeof for primitives, Array.isArray for arrays, and instanceof for custom classes within the same realm.Medium Level Questions
11. What is the purpose of the map() method in JavaScript?
11. What is the purpose of the map() method in JavaScript?
map() vs. forEach() vs. reduce() vs. for loops — and the performance implications of each.Answer:map() creates a new array by applying a transformation function to each element of the source array. It’s the most fundamental functional programming method in JavaScript — it transforms data without side effects (pure function pattern).Core behavior:map() vs. alternatives:map() creates a new array in memory. If you’re chaining map().filter().map(), each intermediate array is allocated and then garbage collected. For hot paths, a single reduce() or a for loop that does everything in one pass is more efficient. Libraries like Lodash offer _.chain with lazy evaluation, and transducers (from Ramda) compose transformations without intermediate arrays.Red flag answer: “map loops through an array.” That describes forEach. The key distinction is that map returns a new array of transformed values. Candidates who can’t distinguish map from forEach, or who use map for side effects, show a gap in functional programming understanding.Follow-up questions:Q: What’s the difference between map() and flatMap()?flatMap() is equivalent to map() followed by flat(1) — it maps each element, then flattens the result by one level. This is useful when your mapping function returns arrays and you want a single flat result. Example: ['hello world', 'foo bar'].flatMap(s => s.split(' ')) gives ['hello', 'world', 'foo', 'bar'] instead of [['hello', 'world'], ['foo', 'bar']]. Real use case: extracting tags from blog posts where each post has multiple tags: posts.flatMap(p => p.tags) gives a flat array of all tags across all posts. flatMap is more efficient than .map().flat() because it only makes one pass.Q: How does map() handle sparse arrays (holes)?map() preserves holes in sparse arrays — it skips empty slots. [1, , 3].map(x => x * 2) returns [2, empty, 6], not [2, undefined, 6]. The callback is never called for the empty slot. This is different from [1, undefined, 3].map(x => x * 2) which returns [2, NaN, 6] because undefined is an actual value. In practice, sparse arrays are rare and usually a bug. If you encounter them, use Array.from to convert holes to undefined first.Q: Can you implement Array.prototype.map from scratch?i in this to skip sparse array holes, it accepts a thisArg parameter for the callback’s this context, and it creates the result array with the same length upfront. This is a common senior-level interview question that tests your understanding of the method’s contract.12. What is event bubbling and event capturing in JavaScript?
12. What is event bubbling and event capturing in JavaScript?
stopPropagation vs stopImmediatePropagation, and performance optimization in complex UIs.Answer:Event propagation in the DOM has three phases, executed in order:- Capturing phase (top to bottom): Event travels from
windowdown to the target element - Target phase: Event reaches the target element
- Bubbling phase (bottom to top): Event travels back up from target to
window
true or { capture: true }):event.target vs event.currentTarget:- React’s event system: React doesn’t attach listeners to individual elements. It uses event delegation on the root container (since React 17, it’s the root DOM node, not
document). All events bubble up to the root where React’s synthetic event system handles dispatch. This is whye.stopPropagation()in React stops propagation within React’s tree, not the native DOM. - Performance in complex UIs: A dropdown with 1,000 items doesn’t need 1,000 click listeners. One listener on the container using event delegation handles them all.
target vs currentTarget, stopPropagation vs stopImmediatePropagation, and practical use cases like event delegation.Follow-up questions:Q: Not all events bubble. Which common events don’t bubble, and how do you handle them?focus, blur, mouseenter, mouseleave, load, unload, scroll (on specific elements), and resize do not bubble. The focusin/focusout events were created as bubbling alternatives to focus/blur. For events that don’t bubble, you can still use the capturing phase to intercept them at a parent level: parent.addEventListener('focus', handler, true). This is how React handles focus events via delegation — it listens during the capture phase. The scroll event on a specific element doesn’t bubble, but you can listen for it in the capture phase on a parent. load events on images and scripts also don’t bubble.Q: How does event.preventDefault() differ from event.stopPropagation()?Completely orthogonal concepts. preventDefault() stops the browser’s default behavior for that event (e.g., preventing a link from navigating, preventing a form from submitting, preventing a checkbox from toggling). stopPropagation() stops the event from reaching other elements but doesn’t prevent the default behavior. You can use both together or independently. A common mistake: calling stopPropagation() on a form submit thinking it prevents submission — it doesn’t; you need preventDefault() for that. Also, return false in a jQuery event handler does BOTH (prevents default + stops propagation), but in vanilla JS, return false from an addEventListener handler does nothing.Q: What’s the { passive: true } option and why does Chrome warn about it?{ passive: true } tells the browser that the event handler will never call preventDefault(). This matters for touch/scroll events because the browser normally has to wait for the handler to complete to know if it should scroll or not (because the handler might call preventDefault()). With passive: true, the browser can scroll immediately without waiting, resulting in smoother scrolling. Chrome added this as the default for touchstart and touchmove on the document level. If you add a non-passive touch handler that calls preventDefault(), Chrome will ignore the preventDefault() and log a console warning. This was a deliberate performance optimization — Google found that most touch handlers never call preventDefault(), so making passive the default improved scroll performance across the web. To opt out: { passive: false }.13. What are higher-order functions? Can you give an example?
13. What are higher-order functions? Can you give an example?
map/filter/reduce as examples without any real-world patterns. Strong candidates connect HOFs to middleware, React HOCs, decorators, memoization, and function composition.Follow-up questions:Q: How do React Higher-Order Components (HOCs) use this pattern, and why has the community moved toward hooks?React HOCs are functions that take a component and return a new enhanced component: const Enhanced = withAuth(MyComponent). They use the HOF pattern to inject props, handle authentication, add logging, etc. The community moved toward hooks because HOCs have problems: “wrapper hell” (deep component nesting visible in DevTools), naming collisions when multiple HOCs inject the same prop name, and difficulty with TypeScript types. Hooks like useAuth() achieve the same code reuse but within the component, without wrapping. That said, HOCs still exist in codebases — React.memo() is essentially a HOC, and connect() from Redux is a HOC factory (a HOF that returns a HOF).Q: What’s the difference between compose and pipe, and when would you use each?Both compose functions left-to-right or right-to-left. compose(f, g, h)(x) executes as f(g(h(x))) — right-to-left, mathematical order. pipe(f, g, h)(x) executes as h(g(f(x))) — left-to-right, reading order. pipe is generally more readable because functions execute in the order you read them. Redux’s compose utility uses right-to-left, which aligns with mathematical function composition. Ramda and RxJS use pipe for readability. In practice, use whichever your team’s tools prefer — consistency matters more than the direction.Q: How would you implement a throttle function, and how does it differ from debounce?Debounce waits until the input has stopped for N milliseconds, then fires once. Throttle fires at most once every N milliseconds, regardless of how many times the function is called. Use debounce for search inputs (fire after user stops typing). Use throttle for scroll handlers or resize handlers (fire at a consistent rate during continuous events). Implementation: function throttle(fn, limit) { let waiting = false; return function(...args) { if (!waiting) { fn.apply(this, args); waiting = true; setTimeout(() => { waiting = false; }, limit); } }; }. Libraries like Lodash provide both with additional options like leading and trailing edge execution.14. What is an IIFE (Immediately Invoked Function Expression) in JavaScript?
14. What is an IIFE (Immediately Invoked Function Expression) in JavaScript?
import/export), JavaScript had no module system. Every var in a script tag was global. If two scripts both declared var helper = ..., one would overwrite the other. IIFEs solved this by creating function scope:import/export) available, are IIFEs obsolete?Not entirely. ES modules solve the primary use case (scope isolation), so you rarely need IIFEs in modern module-based code. But they’re still useful in several scenarios: (1) Immediately-invoked async functions when top-level await isn’t available; (2) Inline complex constant initialization (the const config = (() => {...})() pattern); (3) UMD (Universal Module Definition) bundles that need to work in both script tags and module systems — tools like Webpack and Rollup still output IIFEs for <script> tag consumption; (4) Bookmarklets and browser console snippets where you want no global pollution. So IIFEs are less common but not dead.Q: What’s the difference between (function(){})() and (function(){}())?Both work identically. The parentheses placement differs: the first wraps the function expression, then calls it. The second wraps the entire call expression. Douglas Crockford (creator of JSLint) preferred the second form, calling the first form “dog balls” style. In practice, ESLint’s wrap-iife rule lets you enforce one or the other. Most modern code uses the first form. The real reason for the outer parentheses: without them, function(){}() is parsed as a function declaration (which requires a name) followed by an empty grouping operator, causing a SyntaxError. The parentheses force the parser to treat it as an expression.Q: How does the Revealing Module Pattern improve on the basic Module Pattern?The basic module pattern returns methods that directly reference closure variables. The Revealing Module Pattern defines all functions as private, then returns an object that maps public names to private functions. This makes it easier to see what’s public vs. private and enables renaming public APIs without changing internal function names:export statements.15. What do you understand by closures in JavaScript?
15. What do you understand by closures in JavaScript?
--inspect flag with Node.js, connect Chrome DevTools, take heap snapshots before and after simulated traffic, and compare retained sizes. Look for objects with “(closure)” in the retainers path. Tools like Clinic.js Doctor can automatically detect memory growth. Prevention: be explicit about what variables your closures capture, avoid closuring over large objects, and use WeakRef/WeakMap when appropriate.Q: What is the difference between lexical scope and dynamic scope, and which does JavaScript use?JavaScript uses lexical (static) scope — a function’s scope is determined by where it’s written in the source code, not where it’s called from. This is why closures work: the inner function’s scope chain is fixed at definition time. Dynamic scope (used by some shell languages like Bash) determines scope based on the call stack at runtime. If JavaScript had dynamic scope, closures wouldn’t work as we know them — outerVariable in the example above would not be accessible because the scope would be determined by who calls the function, not where it was defined. The this keyword in JavaScript is the closest thing to dynamic scoping — it’s determined by how a function is called, not where it’s defined (except for arrow functions, which lexically bind this).Q: How does the JavaScript engine optimize closures in practice?Modern engines like V8 perform “scope analysis” during compilation. If a variable in the outer scope is never referenced by any inner function, it won’t be included in the closure’s environment record — it can be garbage collected normally. V8 also creates “context objects” that only contain the variables actually needed by closures, not the entire scope. However, eval() defeats this optimization because the engine can’t know at compile time which variables eval might reference, so it must keep the entire scope alive. This is one reason eval() is avoided in production code beyond security concerns — it prevents closure optimization. You can verify this in Chrome DevTools: set a breakpoint inside a closure and inspect the “Scope” panel to see exactly which variables are captured.16. How do setTimeout() and setInterval() work?
16. How do setTimeout() and setInterval() work?
setInterval drift.Answer:Both are Web APIs (not part of the JavaScript language itself) that schedule callbacks to run after a delay. The critical insight: the delay is a minimum, not a guarantee. The callback runs only when the call stack is empty AND the delay has elapsed.setTimeout() — single delayed execution:setInterval() — repeated execution at intervals:setTimeout(fn, 0) pattern is used to defer execution until after the current call stack clears, not to execute “immediately.” The browser also enforces a minimum delay of ~4ms for nested timeouts (per the HTML spec).The setInterval drift problem:setTimeout(fn, 100) guarantees execution at exactly 100ms, they don’t understand JavaScript’s concurrency model. Also a red flag: using setInterval without clearInterval (memory leak) or not knowing about drift.Follow-up questions:Q: What’s the difference between setTimeout(fn, 0) and queueMicrotask(fn) or Promise.resolve().then(fn)?All three defer execution, but they go into different queues. setTimeout(fn, 0) adds to the macrotask queue (also called the task queue). queueMicrotask(fn) and Promise.resolve().then(fn) add to the microtask queue. Microtasks are processed BEFORE the next macrotask — meaning they run sooner. The execution order is: current synchronous code finishes, then ALL microtasks drain (including any microtasks queued by microtasks), then ONE macrotask runs, then microtasks again, and so on. This means Promise.resolve().then(() => console.log('micro')) always fires before setTimeout(() => console.log('macro'), 0). This is critical knowledge for understanding React state batching and avoiding render timing bugs.Q: How do timers behave in inactive browser tabs?Browsers throttle timers in background tabs. In Chrome, setInterval in a background tab is throttled to fire at most once per second (instead of the specified interval). setTimeout with delays less than 1000ms are delayed to 1000ms minimum. This is a deliberate battery and CPU optimization. This breaks polling-based features (like live dashboards) when users switch tabs. Solutions: use the Page Visibility API (document.addEventListener('visibilitychange', ...)) to pause/resume polling, or use Web Workers which are NOT throttled in background tabs. Some apps switch from polling to WebSocket push when the tab goes to background.Q: How would you implement a rate limiter using setTimeout?17. Explain the concept of Promises in JavaScript
17. Explain the concept of Promises in JavaScript
Promise.all vs Promise.allSettled vs Promise.race vs Promise.any, and the ability to work with real-world async flows.Answer:A Promise is an object representing the eventual completion or failure of an asynchronous operation. It’s the foundation of modern async JavaScript — async/await is built on Promises, and virtually every I/O operation in Node.js and browser APIs returns one.The three states (immutable once settled):- Pending: Initial state, operation in progress
- Fulfilled: Operation succeeded, has a result value
- Rejected: Operation failed, has a reason (error)
.then() and .catch(). Candidates who can’t explain Promise.allSettled vs Promise.all, or who don’t know about Promise.race for timeouts, are missing practical async patterns. Another red flag: not knowing that unhandled rejections crash Node.js.Follow-up questions:Q: What happens if you resolve a Promise with another Promise?The Promise specification (Section 2.3.2) says if resolve(value) is called where value is itself a thenable (has a .then method), the outer Promise “adopts” the state of the inner Promise. So new Promise(resolve => resolve(Promise.resolve(42))) eventually fulfills with 42, not with a Promise object. This is called “recursive unwrapping” and it means you can’t wrap a Promise inside another Promise — they flatten automatically. This is different from reject, which does NOT unwrap: new Promise((_, reject) => reject(Promise.resolve(42))) rejects with the Promise object itself.Q: How do Promises relate to the microtask queue and why does it matter?Promise .then/.catch/.finally callbacks are scheduled as microtasks, not macrotasks. Microtasks execute BEFORE the next macrotask (setTimeout, I/O callbacks, etc.) and BEFORE the browser renders. This means if you create a chain of 10,000 .then callbacks, they all execute before the browser can update the UI — potentially causing UI jank. In Node.js, process.nextTick has even higher priority than microtasks. Understanding this priority order is essential for debugging timing issues. Example: setState in React batches updates within the same microtask checkpoint, which is why multiple setState calls in a Promise chain behave differently than multiple setState calls in setTimeout callbacks (before React 18’s automatic batching).Q: How would you implement a retry mechanism with exponential backoff using Promises?Math.random() * 1000) prevents thundering herd — if 1,000 clients all retry at exactly the same backoff intervals, they’ll overwhelm the server simultaneously. AWS recommends this pattern in their architecture guidelines.18. What is the use of async and await in JavaScript?
18. What is the use of async and await in JavaScript?
async/await provides synchronous-looking syntax for asynchronous operations. An async function always returns a Promise. await pauses the function’s execution until the awaited Promise settles, then resumes with the resolved value.How it works under the hood:Promise.all for parallel execution, or writing sequential awaits for independent operations and not recognizing the performance impact.Follow-up questions:Q: What happens if you forget to await a Promise inside an async function?The function continues executing without waiting for the Promise to resolve. This is a common bug — you get a Promise object instead of the resolved value. Example: const data = fetchUser() gives you a Promise, not user data. if (data.name) is always truthy (the Promise object exists). The insidious part: no error is thrown at this point, so the bug silently produces wrong behavior. Worse, if the unawaited Promise rejects, you get an unhandled rejection. ESLint rules like @typescript-eslint/no-floating-promises and require-await catch this at lint time. TypeScript’s type system also helps — data would be typed as Promise<User> not User, so data.name would be a type error.Q: How do you handle errors differently for parallel operations with Promise.all?Promise.all fails fast — if any Promise rejects, the entire result is rejected, and you lose the results of successful Promises. Three approaches: (1) Promise.allSettled — lets all Promises settle, then you inspect each result’s status field. (2) Wrap each Promise with a catch that converts rejections to a known error shape: Promise.all(urls.map(url => fetch(url).catch(err => ({ error: err, url })))). (3) For critical + non-critical mixed: await critical ones with Promise.all, and non-critical ones with Promise.allSettled. In production, the right choice depends on whether partial results are useful. Loading a user’s profile (critical) and their notification count (non-critical) — use approach 3.Q: Can you use await outside of an async function?Only in ES modules with top-level await (Node.js 14.8+ with ESM, modern browsers). In CommonJS (require), top-level await is not available. The workaround is wrapping in an async IIFE: (async () => { const data = await fetch(...); })();. Top-level await has an important implication for module loading: any module that imports from a module using top-level await will wait for that await to resolve before executing. This can create waterfall loading patterns if overused. Use it for initialization (database connections, config loading) but not for lazy operations that could block module graph resolution.19. What is the difference between call(), apply(), and bind()?
19. What is the difference between call(), apply(), and bind()?
this binding in JavaScript, the ability to explain why these methods exist (dynamic this context), and practical use cases beyond textbook examples — method borrowing, partial application, and event handler binding.Answer:All three methods allow you to explicitly set the this context of a function. They exist because JavaScript’s this is determined by how a function is called, not where it’s defined — and sometimes you need to override that behavior.| Method | Execution | Arguments | Returns |
|---|---|---|---|
call() | Immediate | Individual: fn.call(ctx, a, b) | Function’s return value |
apply() | Immediate | Array: fn.apply(ctx, [a, b]) | Function’s return value |
bind() | Deferred | Individual (partial): fn.bind(ctx, a) | New function |
call() — invoke immediately with explicit this:apply() — same as call but arguments as array:bind() — returns a new function with fixed this (and optionally fixed args):this problem, give real use cases, and mention that arrow functions have largely replaced bind for the most common use case.Follow-up questions:Q: How do arrow functions change the need for bind, and what are the tradeoffs?Arrow functions lexically capture this from their enclosing scope — they don’t have their own this. This eliminates the most common use of bind: fixing this in callbacks and event handlers. In React, onClick={() => this.handleClick()} or class field arrow functions handleClick = () => {...} replaced the constructor bind pattern. Tradeoffs: (1) Arrow functions can’t be used as constructors (no new), (2) They don’t have arguments object, (3) They can’t be used as methods on objects if you need this to be dynamic: const obj = { name: 'A', getName: () => this.name } — this is the outer scope, not obj. (4) In class components, arrow function class fields create a new function per instance (not on the prototype), using more memory if you have thousands of instances.Q: What is Function.prototype.bind doing under the hood? Can you implement it?new check is the tricky part — a bound function can still be used as a constructor with new, and in that case, the bound this is ignored in favor of the newly created object. This is specified in the ECMAScript standard and tested in interviews to check deep understanding.Q: In what situations would this be undefined even without strict mode?In strict mode, this is undefined for plain function calls (no object context). But even in sloppy mode, this can be undefined in ES modules (which are always strict). Arrow functions in the global scope of a module have this === undefined. Another case: destructured methods lose their context — const { method } = obj; method() — this is undefined in strict mode. This is a common React bug when destructuring event handlers from a context object.20. What is event delegation?
20. What is event delegation?
closest() method is essential (not just e.target):- React: Since React 17, all events are delegated to the root DOM node (not
document). This enables multiple React roots on the same page without event interference. - jQuery:
$(parent).on('click', '.child', handler)— jQuery’s delegation syntax. - Vue:
v-ondirectives attach directly to elements (no delegation by default), but libraries likevue-delegated-eventsadd it for performance-critical lists.
closest() instead of just e.target.tagName, and be aware of the contains() guard pattern.Follow-up questions:Q: What are the limitations of event delegation?(1) Events that don’t bubble (focus, blur, scroll, mouseenter, mouseleave) can’t be delegated in the bubbling phase — you’d need the capture phase. (2) stopPropagation() called by any intermediate handler breaks delegation for that event. (3) There’s a slight performance overhead per event — the closest() or matches() check runs on every click, even irrelevant ones. For a form with 3 inputs, direct listeners are simpler and faster. Delegation shines when you have many similar elements or dynamic content. (4) Some CSS pseudo-elements (like ::before, ::after) can’t be event targets at all. (5) Debugging is harder because DevTools shows the listener on the parent, not the child, making it less obvious which element’s click is being handled.Q: How would you implement event delegation that supports multiple event types and namespaced handlers?.namespace suffixes for grouped removal) and one-time handlers.Hard Level Questions
21. Explain the event loop in JavaScript
21. Explain the event loop in JavaScript
- Execute all synchronous code (call stack)
- Drain the entire microtask queue (ALL microtasks, including ones added during processing)
- Execute ONE macrotask
- Back to step 2 (check microtasks again)
- (Browser only) Render/paint if needed (~16ms for 60fps)
- Sync code runs: “1: sync”, “7: sync end”
- Call stack empty: drain microtasks — “3: promise 1”, “4: promise 2”
- “4: promise 2” adds a nested Promise (microtask) and setTimeout (macrotask)
- Nested Promise is a microtask, so drain continues: “6: nested promise”
- Microtask queue now empty. Pick one macrotask: “2: setTimeout”
- Microtask queue empty. Pick next macrotask: “5: nested setTimeout”
- UI blocking: A Promise chain with 100,000
.thencallbacks blocks the browser from rendering because microtasks drain completely before paint. UserequestAnimationFrameor chunking. - Starvation: Infinite microtasks (recursive
queueMicrotask) starve macrotasks — yoursetTimeoutcallbacks never run. - Node.js performance:
process.nextTickin a recursive loop can starve I/O. UsesetImmediateinstead for yielding to the event loop.
requestAnimationFrame, setTimeout, and queueMicrotask in terms of timing?queueMicrotask(fn) fires at the end of the current task, before any rendering or macrotasks. setTimeout(fn, 0) fires in the next macrotask cycle, after rendering if needed. requestAnimationFrame(fn) fires just before the next browser repaint (~16ms at 60fps). For visual updates: always use requestAnimationFrame — it syncs with the display’s refresh rate and is batched efficiently. For DOM reads followed by DOM writes, requestAnimationFrame prevents layout thrashing. For logic that should run “soon” without blocking rendering: use setTimeout(fn, 0). For logic that must run before the next render: use queueMicrotask. Note: requestAnimationFrame doesn’t exist in Node.js.Q: How can microtask starvation happen and how do you prevent it?If a microtask queues another microtask, which queues another, the microtask queue never empties. No macrotasks execute, no rendering occurs — the UI freezes. Example: a recursive Promise.resolve().then(recursiveFunction). Prevention: if processing a large dataset asynchronously, batch work and yield to the event loop with setTimeout(fn, 0) between batches (not queueMicrotask, which doesn’t yield). Pattern: process 1000 items, then setTimeout(processNext1000, 0) to let the browser breathe. React’s useTransition and the Scheduler package implement this cooperative yielding pattern to keep the UI responsive during large state updates.Q: A developer reports that their setTimeout(fn, 100) is actually firing at 200-300ms. What could cause this?Several possibilities: (1) Long-running synchronous code on the main thread — the timer fires after the delay, but the callback can’t run until the stack is empty. A complex React render or heavy computation blocks it. (2) Heavy microtask processing (many resolved Promises draining before the macrotask). (3) Browser tab is in the background — Chrome throttles timers to 1000ms minimum for background tabs. (4) The system is under heavy load (CPU-bound). (5) Nested setTimeout calls in the same context — the HTML spec requires a minimum 4ms delay after 5 nested levels. (6) Garbage collection pauses (can be 10-50ms for major GC in V8). Diagnosis: use performance.now() before and after, check the Performance tab in DevTools for long tasks, or use the PerformanceObserver API to detect long tasks programmatically.22. What is the difference between Promises and async/await?
22. What is the difference between Promises and async/await?
async/await is syntactic sugar built on top of Promises — the underlying mechanism is identical. The difference is in readability, error handling ergonomics, and code flow. Neither replaces the other; they’re complementary.Comparison across dimensions:| Aspect | Promise .then chains | async/await |
|---|---|---|
| Readability | Nesting/chaining can be complex | Linear, synchronous-looking flow |
| Error handling | .catch() — catches async errors in chain | try/catch — familiar block syntax |
| Debugging | Stack traces can be unclear | Stack traces show the await point |
| Parallel execution | Promise.all([...]) — explicit | Easy to accidentally write sequential code |
| Conditional logic | Awkward nested .then chains | Clean if/else with await |
| Loop integration | Difficult with .then in loops | Natural for/while with await |
all, allSettled, race, any) have no async/await equivalent — you still need them. Also, some patterns (like streaming or pipeline-style processing) read more naturally with .then chains. A strong engineer knows when each is appropriate.Follow-up questions:Q: Why is return await promise sometimes necessary inside a try/catch but otherwise redundant?Without try/catch, return await promise and return promise are functionally identical — the async function returns a Promise either way. But inside try/catch, there’s a crucial difference: return promise forwards the Promise directly, so if it rejects, the rejection bypasses the catch block entirely (the async function’s returned Promise rejects). return await promise unwraps the Promise inside the async function’s scope, so a rejection triggers the catch block. ESLint has a no-return-await rule that flags unnecessary return await, but the rule is disabled inside try/catch because there it’s necessary.Q: How do you handle concurrent async operations with a concurrency limit?Promise.all runs everything in parallel with no limit. For rate-limited APIs (Stripe: 25 req/s, GitHub: 5000 req/hr), you need controlled concurrency:p-limit (300K+ weekly downloads) provide this. In production at scale, you’d also add per-second rate limiting and circuit breaker patterns.Q: What are async generators and for await...of? When would you use them?Async generators combine generators and async functions — they yield Promises. for await...of consumes async iterables. Use case: processing data that arrives over time (streaming HTTP responses, WebSocket messages, database cursors, paginated APIs). Example: Node.js Readable streams implement the async iterator protocol, so you can do for await (const chunk of readStream). Without this, you’d have complex event-based code with on('data') handlers. The pattern is natural for ETL pipelines: read from a stream, transform each chunk, write to output — all with backpressure handling built into the iteration protocol.23. Describe the purpose of the reduce() method in arrays
23. Describe the purpose of the reduce() method in arrays
reduce for complex transformations (grouping, pivoting, pipeline building), understand when reduce is overkill vs. the right tool, and can reason about the accumulator pattern.Answer:reduce() processes an array element-by-element, accumulating a result. Unlike map (same-length array) and filter (subset array), reduce can produce ANY output type — a number, string, object, array, or even a function.Syntax and mental model:reduce is the most powerful and versatile array method — candidates who can only sum numbers with it haven’t used it in real code. Another red flag: creating new arrays inside reduce with spread ([...acc, item]) which is O(n^2) because spread copies the entire array on each iteration.Follow-up questions:Q: What’s the performance issue with [...acc, item] inside reduce, and how do you fix it?Using spread inside reduce creates a new array on every iteration. For an array of n items, you copy 1, then 2, then 3… items = n*(n+1)/2 = O(n^2) total operations. For 100,000 items, this is ~5 billion copy operations. The fix: use acc.push(item); return acc; which is O(1) per iteration, O(n) total. Yes, push mutates the accumulator, but since the accumulator was created by reduce (the initial []), this is safe. Or better yet, just use .map() or .filter() which are already O(n). This performance difference matters at scale — I’ve seen production code where a reduce with spread on a 50K-item array took 3 seconds in Chrome.Q: How does reduceRight work and when would you use it?reduceRight processes from right to left (last element to first). The classic use case is function composition: const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x). This gives mathematical composition order where compose(f, g)(x) equals f(g(x)). It’s also useful when building strings or structures where the last element should be the innermost wrapper. In practice, reduceRight is rare — most use cases are covered by reduce with reversed logic or by using pipe (left-to-right composition) instead of compose.Q: How does Object.groupBy compare to the reduce grouping pattern?Object.groupBy(array, keyFn) (shipping in 2024, supported in Node 21+ and modern browsers) does exactly what the reduce grouping pattern does but in a single, declarative call. Object.groupBy(transactions, t => t.category) replaces 5+ lines of reduce code. It returns a null-prototype object (no inherited properties). There’s also Map.groupBy for when you need a Map. This is a case where the language caught up to the most common reduce pattern. However, for more complex aggregation (grouping AND summing, or grouping into custom structures), you still need reduce.24. Explain the concept of currying in JavaScript
24. Explain the concept of currying in JavaScript
React.createElement and JSX?JSX <Component prop="value" /> compiles to React.createElement(Component, { prop: 'value' }). Higher-order component factories are essentially curried functions: const withAuth = (requiredRole) => (Component) => (props) => { ... }. Usage: const AdminPage = withAuth('admin')(Dashboard). This is currying in practice — fix the role, get a component wrapper, then it receives props at render time. Hooks partially replaced this pattern but currying still appears in createSlice (Redux Toolkit), styled() (styled-components), and connect(mapState, mapDispatch)(Component) (React-Redux).Q: What is “point-free style” and how does currying enable it?Point-free (or tacit) programming means defining functions without explicitly mentioning their arguments. Currying makes this possible because partially-applied functions are already ready to accept remaining arguments. Example: instead of users.map(user => formatName(user)), you write users.map(formatName) — point-free. With curried utilities: instead of data.filter(item => item.age > 18), a curried greaterThan lets you write data.filter(greaterThan(18)). Ramda and lodash/fp provide auto-curried utilities enabling this style. The tradeoff: point-free code can be elegant or cryptic depending on the team’s familiarity with the pattern. Use it where it genuinely improves readability.Q: Why are curried functions particularly useful in functional composition?Curried functions are composable because they always return a function until all arguments are provided. This means you can build complex operations by chaining simple, single-purpose functions. Example: const processUser = pipe(getAge, greaterThan(18), not) creates a function that checks if a user is underage, without ever writing function(user) { return !(user.age > 18); }. Each piece is reusable and testable in isolation. Libraries like Ramda are built entirely on this principle — every function is auto-curried and data-last, making composition the default.25. What is a generator function and how is it used?
25. What is a generator function and how is it used?
for-await-of loop.Answer:A generator function (declared with function*) can pause its execution at yield points and resume later. It returns an iterator object that you control with next(). This enables lazy evaluation, custom iteration, and cooperative multitasking patterns.How generators work under the hood:yield* for delegation:yield 1; yield 2; yield 3 without practical use cases, they haven’t used generators in production.Follow-up questions:Q: How did generators relate to async/await before async/await existed?Before ES2017, libraries like co (by TJ Holowaychuk, creator of Express) used generators to simulate async/await. The idea: yield a Promise, and the library’s runner would await it and send the result back via next(resolvedValue). The pattern: co(function* () { const data = yield fetch('/api'); return data; }). This is exactly what async/await does under the hood — in fact, Babel initially transpiled async/await to generator-based code. Understanding this history helps you see that generators are a more general mechanism than async/await.Q: What is the difference between generator.return() and generator.throw()?generator.return(value) forces the generator to complete immediately — the next call to next() returns { done: true }. It triggers any finally blocks inside the generator. This is how for...of cleans up: when you break out of a loop, it calls return() on the iterator. generator.throw(error) injects an error at the current yield point — the generator can catch it with try/catch and continue, or let it propagate. This enables error injection for testing generators and handles errors in async-generator-based flows.Q: What are async generators and when would you use them over regular generators?Async generators (async function*) can use await inside them and yield Promises. They’re consumed with for await...of. Use case: streaming data processing where each item requires async work. Example: reading a file line by line, making an API call for each line, and yielding results. Node.js streams implement the async iterator protocol, so for await (const chunk of readableStream) works. Without async generators, you’d use complex event-based code with on('data') callbacks and manual backpressure management. The for await...of loop handles backpressure naturally — it won’t request the next value until the current one is processed.26. What are WeakMap and WeakSet in JavaScript?
26. What are WeakMap and WeakSet in JavaScript?
| Feature | Map/Set | WeakMap/WeakSet |
|---|---|---|
| Key types | Any value | Objects only (no primitives) |
| References | Strong (prevents GC) | Weak (allows GC) |
| Iterable | Yes (for...of, forEach) | No |
.size | Yes | No |
| Deterministic | Yes | No (GC timing varies) |
| Use case | General storage | Object metadata, caching |
weakMap.set(42, 'value'), there’s no “object” to hold a weak reference to — the number 42 isn’t allocated on the heap in a way that can be garbage collected. A primitive value just IS — it doesn’t have a lifecycle. WeakMap’s entire purpose is to tie data to an object’s lifecycle, and primitives don’t have lifecycles. WeakRef (ES2021) follows the same logic — you can only create weak references to objects.Q: What is WeakRef and FinalizationRegistry, and how do they extend the weak reference concept?WeakRef (ES2021) lets you hold a weak reference to an individual object (not just in a Map/Set context) via new WeakRef(obj). You call ref.deref() to get the object back — it returns undefined if the object has been GC’d. FinalizationRegistry lets you register a callback that runs after an object is garbage collected (a “destructor” pattern). Use case: resource cleanup for objects that hold native resources (file handles, WebGL buffers, database connections). Warning: GC timing is non-deterministic, so never use these for critical logic — only for optimization and resource cleanup. The TC39 proposal explicitly warns against relying on finalization for correctness.Q: How would you implement an LRU cache, and would you use WeakMap for it?A WeakMap is NOT suitable for LRU caches because you can’t iterate it (no way to find the least recently used entry) and you can’t control eviction. An LRU cache needs: (1) O(1) get/set, (2) tracking access order, (3) evicting the oldest entry when capacity is reached. The standard implementation uses a combination of a Map (which maintains insertion order) and manual eviction: when the Map exceeds capacity, delete the first key (map.keys().next().value). For production, libraries like lru-cache (80M+ weekly npm downloads) provide battle-tested implementations with TTL, stale-while-revalidate, and size-based eviction. WeakMap is for “cache that cleans itself when the key disappears” — different from LRU where you want to control the eviction policy.27. How does JavaScript handle memory management?
27. How does JavaScript handle memory management?
--max-old-space-size).Garbage Collection in V8 (Chrome/Node.js):V8 uses a generational garbage collector with two main spaces:1. Young Generation (Scavenger — Minor GC):- Small space (~1-8MB), for recently created objects
- Uses a semi-space (copy) algorithm: two halves, objects are copied from “from-space” to “to-space” if still alive
- Very fast (~1-2ms pauses)
- Runs frequently (every few hundred milliseconds)
- Most objects die young (the “generational hypothesis”)
- Larger space (up to 1.5GB), for objects that survived multiple young GC cycles
- Mark phase: Traverse from roots (global scope, stack, handles), mark reachable objects
- Sweep phase: Free memory of unmarked objects
- Compact phase: Defragment memory (move objects to reduce fragmentation)
- Slower (10-50ms pauses, can be 100ms+ for large heaps)
- V8 does this incrementally and concurrently to minimize pauses
process.memoryUsage() logging at intervals to confirm the leak (heap growing, not just RSS). Step 2: Use --inspect flag and connect Chrome DevTools remotely (or use node --heapsnapshot-signal=SIGUSR2 to take snapshots in production without DevTools). Step 3: Take heap snapshots at T=0, T=1hr, T=4hr. Compare snapshots looking for: (a) Objects that keep growing in count, (b) Large retained sizes, (c) Strings or arrays with growing counts. Step 4: Use the “Allocation timeline” in DevTools to see what’s being allocated over time. Common culprits: growing Maps/Arrays used as caches without eviction, event listeners on long-lived emitters that accumulate per-request, closures holding references to request/response objects in middleware. Tools: Clinic.js, Heapdump, 0x for production profiling.Q: What is the difference between RSS, heap total, heap used, and external memory in Node.js?rss (Resident Set Size) is total memory allocated by the OS to the process — includes heap, stack, code segment, and shared libraries. heapTotal is V8’s total heap allocation (may include unused pre-allocated space). heapUsed is how much of the heap is actually occupied by live objects. external is memory used by C++ objects bound to JavaScript (Buffers, native addons). A common pattern: heapUsed stays flat but rss grows — this indicates a native memory leak (Buffers not being freed, or C++ addon leaks). heapUsed growing indicates a JavaScript-level leak (objects not being GC’d). external growing often points to Buffer leaks in stream processing.Q: How does ArrayBuffer and SharedArrayBuffer memory differ from regular heap memory?ArrayBuffer allocates raw binary memory, tracked under V8’s “external” memory (not the JavaScript heap). This memory is allocated by the OS and managed by V8’s GC only through the associated ArrayBuffer wrapper. If you create many small ArrayBuffers, the GC might not trigger often enough because the JS heap looks small while external memory grows. You can use --max-old-space-size for heap limits but external memory has no built-in limit. SharedArrayBuffer is similar but shareable between Workers — it requires explicit synchronization with Atomics (compare-and-swap, wait/notify). SharedArrayBuffer was disabled in browsers after Spectre/Meltdown and re-enabled only with proper COOP/COEP headers for cross-origin isolation.28. Describe the difference between shallow copy and deep copy
28. Describe the difference between shallow copy and deep copy
structuredClone (the modern solution most candidates miss), and when copying matters in practice (React state, Redux immutability).Answer:The distinction matters whenever you work with nested objects or arrays. A shallow copy duplicates the top-level properties; a deep copy recursively duplicates everything including nested objects.The reference problem:structuredClone() — THE modern solution (2022+):JSON.parse(JSON.stringify()) — the classic hack:JSON.parse(JSON.stringify()) for deep copy.” Without mentioning its limitations (loses functions, Dates, undefined, circular refs), this is a dangerous recommendation. Strong candidates know about structuredClone and its limitations, and mention Immer for React/Redux contexts.Follow-up questions:Q: What is “structural sharing” and why is it better than deep copying for state management?Structural sharing creates new objects only for the parts of the tree that changed, reusing references for unchanged subtrees. If you have { a: { x: 1 }, b: { y: 2 } } and update a.x, structural sharing creates a new root object and a new a object, but b is the SAME reference. This is O(depth) instead of O(total nodes) for memory and time. Immutable.js and Immer both use this. It also enables efficient equality checks — React can do prevState.b === nextState.b to skip re-rendering components that depend on b. Without structural sharing, deep copying large state trees on every Redux action would be prohibitively expensive.Q: How do circular references affect copying, and how do you handle them?JSON.stringify throws a TypeError on circular references. structuredClone handles them correctly — it tracks visited objects and recreates the circular structure in the clone. Lodash’s cloneDeep also handles circular references. If you’re writing a custom deep clone, you need a WeakMap (or Map) to track visited objects: if (visited.has(obj)) return visited.get(obj);. The visited map maps original objects to their clones, so when you encounter the same object again, you return the existing clone instead of recursing infinitely.Q: When would you use Object.freeze vs. deep copying for immutability?Object.freeze is shallow — nested objects are still mutable. It’s a development-time guard, not a deep immutability solution. Use it for configuration objects that shouldn’t be modified: const CONFIG = Object.freeze({ apiUrl: '...', timeout: 5000 }). For state management, deep freeze (recursive Object.freeze) is expensive and impractical for large objects — it also prevents any mutation, which breaks patterns like Immer that rely on Proxy-based mutation tracking. In production, the standard approach is: use TypeScript’s Readonly<T> for compile-time immutability, Immer for runtime immutable updates, and Object.freeze only for small, static configuration objects.29. What is JavaScript's strict mode and how is it enabled?
29. What is JavaScript's strict mode and how is it enabled?
'use strict';) opts into a restricted variant of JavaScript that eliminates silent errors, prevents unsafe patterns, and enables future language optimizations. It was introduced in ES5 (2009) to fix decades-old language design mistakes that couldn’t be changed in sloppy mode without breaking the web.Enabling strict mode:this in functions is undefined, not window:delete on non-configurable properties throws:eval doesn’t leak variables:arguments object is decoupled from parameters:- ES modules (
import/export) are always strict — no directive needed - Classes are always strict internally
- Bundlers (Webpack, Vite) typically output modules, so code is strict by default
- Node.js with
"type": "module"in package.json makes all.jsfiles strict
'use strict' matters mainly for: legacy scripts loaded via <script> tags, Node.js CommonJS files, and immediately-invoked function expressions in non-module contexts.Red flag answer: “Strict mode makes JavaScript stricter” without listing specific behaviors. Or not knowing that modules are automatically strict. Another red flag: thinking strict mode has a performance cost — in reality, strict mode enables V8 optimizations (no arguments aliasing, no with statement, predictable this behavior).Follow-up questions:Q: Does strict mode have any performance implications?Strict mode enables certain V8 optimizations. Without strict mode, the engine must handle with statements (which make scope chains unpredictable), arguments aliasing (changes to named params reflect in arguments), and potential global creation on assignment. In strict mode, the engine knows these won’t happen, enabling more aggressive inlining and scope optimization. The performance difference is small (1-5%) but it’s a net positive. There’s no performance cost to strict mode. This is why the V8 team and TC39 continue to make new features strict-only.Q: What happens when strict and non-strict code interact?Each function has its own strictness mode. A strict function called from non-strict code is still strict. A non-strict function called from strict code is still non-strict. However, concatenating strict and non-strict scripts can be problematic — if a strict file is bundled after a non-strict file, the 'use strict' might end up inside a function scope or be preceded by other statements, rendering it ineffective. This is why bundlers wrap each module in its own function scope. IIFE-wrapped libraries often include their own 'use strict' at the top of the IIFE.Q: What are some of the 'use strict' behaviors that were adopted as defaults in ES6+?Several strict-mode-only features became the default in ES6+: (1) Block scoping with let/const (strict-like behavior by default). (2) Classes are always strict. (3) Arrow functions don’t have their own this (no accidental global this). (4) ES modules are always strict. (5) Default parameters, destructuring, and for...of were designed with strict semantics in mind. The direction of the language is clearly toward strict-by-default, and sloppy mode is essentially a legacy compatibility layer. TC39 has stated they won’t add new syntax to sloppy mode.30. Explain the Observer pattern and how it relates to JavaScript
30. Explain the Observer pattern and how it relates to JavaScript
map, filter, debounceTime, switchMap, retry that compose into powerful pipelines. (2) Backpressure handling. (3) Cold vs. hot observables (unicast vs. multicast). (4) Built-in error handling and completion semantics. Use cases: complex event handling (autocomplete search with debounce + switchMap + error retry), WebSocket message processing, combining multiple async sources, and Angular (which uses RxJS extensively). The tradeoff: steep learning curve and large bundle size (~30KB min+gzip). For simple cases, Promises or EventEmitter are sufficient.Q: How would you implement a once listener that also handles errors properly?Expert Level Questions
31. Explain the Proxy and Reflect APIs and their real-world applications
31. Explain the Proxy and Reflect APIs and their real-world applications
Proxy creates a wrapper around an object that intercepts and customizes fundamental operations (property access, assignment, function calls, etc.). Reflect provides the default behavior for those operations, making it easy to call the original operation from within a trap.Basic Proxy with traps:ref() and reactive() work):get, set, has (for in operator), deleteProperty, apply (function calls), construct (new), getPrototypeOf, setPrototypeOf, isExtensible, preventExtensions, defineProperty, getOwnPropertyDescriptor, ownKeys.Performance note: Proxy operations have overhead (roughly 5-10x slower than direct property access in microbenchmarks). For hot paths processing millions of operations per second, this matters. Vue 3 mitigates this by only proxying top-level reactive objects, not deep nesting. For general application code, the overhead is negligible.Red flag answer: “Proxy is for intercepting object access.” Too vague. Candidates should know specific traps, mention Reflect as the companion API, and give at least one production use case (Vue reactivity, validation, or logging).Follow-up questions:Q: Why does Reflect exist alongside Proxy? Can’t you just use target[prop] directly?Reflect methods have the same signature as Proxy traps, making them the clean way to invoke default behavior. More importantly, Reflect.get/set properly handles the receiver parameter for prototype chain correctness. Without Reflect, target[prop] in a get trap can break when the proxy is used as a prototype or when getters use this. Reflect also returns boolean success/failure (for set, defineProperty) instead of throwing, which is consistent with the boolean return expected by Proxy traps.Q: What are Proxy “invariants” and why do they exist?Invariants are constraints that Proxy traps must obey to prevent breaking fundamental JavaScript guarantees. For example: a get trap cannot return a value different from the target’s property if that property is non-configurable and non-writable. A has trap cannot hide a non-configurable own property. These exist to maintain the integrity of the language’s core contracts — without them, a Proxy could lie about Object.freezed properties, making the freeze semantics meaningless. Violating invariants throws a TypeError.32. What are JavaScript Symbols and how are they used in metaprogramming?
32. What are JavaScript Symbols and how are they used in metaprogramming?
Symbol.for), and practical metaprogramming applications.Answer:Symbols are unique, immutable primitive values primarily used as property keys that are guaranteed not to collide with any other key (string or Symbol). They enable both namespacing (avoiding property collisions) and metaprogramming (customizing built-in language behavior).Three categories of Symbols:1. Unique Symbols (privacy and collision avoidance):Symbol.for / Symbol.keyFor):Symbol.iterator, Symbol.toPrimitive, Symbol.hasInstance) are missing the metaprogramming power that makes Symbols a language-level feature rather than just a utility.Follow-up questions:Q: How does React use Symbols internally?React uses Symbol.for('react.element') as the $$typeof property on React elements. This prevents XSS attacks where an attacker could inject a JSON object that looks like a React element. Since Symbols can’t be represented in JSON (JSON.parse can’t create Symbols), a server-rendered object from untrusted data can never have a valid $$typeof. React checks for this Symbol before rendering any element. This is an elegant security pattern: the defense mechanism is built into the data format, not just the rendering logic.Q: What is Symbol.asyncIterator and how does it enable for await...of?Symbol.asyncIterator is the protocol for async iteration. An object implementing [Symbol.asyncIterator]() must return an object with a next() method that returns a Promise of { value, done }. This is what makes for await (const item of asyncIterable) work. Node.js Readable streams implement this protocol, enabling for await (const chunk of fs.createReadStream('file')). You can create custom async iterables for paginated APIs, WebSocket streams, or any data source that produces values over time.33. What is the difference between microtasks and macrotasks, and how does `queueMicrotask` work?
33. What is the difference between microtasks and macrotasks, and how does `queueMicrotask` work?
setTimeout,setIntervalsetImmediate(Node.js)- I/O callbacks
- UI rendering events
MessageChannel,postMessage- One macrotask runs per event loop iteration
- Promise
.then/.catch/.finallycallbacks queueMicrotask(fn)MutationObservercallbacksprocess.nextTick(Node.js, even higher priority than microtasks)- ALL microtasks drain before the next macrotask
queueMicrotask vs. Promise.resolve().then():process.nextTick fit relative to microtasks and macrotasks?process.nextTick callbacks run BEFORE microtasks (Promises). The priority order in Node.js is: (1) Synchronous code, (2) process.nextTick queue, (3) Microtask queue (Promises), (4) Macrotask (timers, I/O). Recursive process.nextTick can starve I/O even harder than microtask recursion because it runs before I/O polling. The Node.js docs explicitly warn against using process.nextTick recursively and recommend setImmediate for yielding to the event loop. The distinction exists for historical reasons and backward compatibility.Q: How does the browser’s rendering fit into the task queue model?Rendering (style calculation, layout, paint) happens BETWEEN macrotasks, after microtasks drain, approximately every 16ms (60fps). requestAnimationFrame callbacks run just before rendering. The sequence: macrotask -> microtasks -> rAF -> render -> next macrotask. This is why long-running microtask chains block rendering — the browser can’t paint until the microtask queue is empty. For smooth animations, schedule work with requestAnimationFrame (synced with display refresh) rather than setTimeout (not synced, can cause dropped frames).34. How does prototypal inheritance work in JavaScript, and how does it differ from classical inheritance?
34. How does prototypal inheritance work in JavaScript, and how does it differ from classical inheritance?
Object.create, how class syntax maps to prototypes, and when composition is preferable to inheritance.Answer:JavaScript uses prototypal inheritance: objects inherit directly from other objects through an internal [[Prototype]] link. There are no classes at the engine level — the class keyword is syntactic sugar over prototypes and constructor functions.The prototype chain:class maps to prototypes:| Aspect | Prototypal (JavaScript) | Classical (Java, C++) |
|---|---|---|
| Mechanism | Objects inherit from objects | Classes define blueprints, instances from classes |
| Flexibility | Can modify prototype at runtime | Class structure fixed at compile time |
| Multiple inheritance | Possible via mixins/Object.assign | Limited (interfaces in Java) |
| Memory | Methods shared on prototype (efficient) | Methods on each instance (or vtable) |
class is syntactic sugar over prototypes, with fundamentally different mechanics (dynamic dispatch, runtime prototype modification, no access modifiers until recent private fields). Candidates who can’t explain the prototype chain or Object.create are relying on class syntax without understanding what it does.Follow-up questions:Q: What is the performance difference between own properties and prototype chain lookups?Own property access is O(1) — a direct hash table lookup on the object. Prototype chain lookup traverses up the chain until the property is found or the chain ends (null). In the worst case, it’s O(chain depth). V8 optimizes this with “hidden classes” (also called “shapes” or “maps”) and inline caches — after the first lookup, V8 caches the lookup path and subsequent accesses are nearly as fast as own property access. However, very long prototype chains (depth 10+) can defeat these optimizations. In practice, keeping chains shallow (2-3 levels) and using hasOwnProperty() checks when needed is the standard practice.Q: How do private class fields (#) work under the hood?Private fields (#field) are not stored on the prototype — they’re stored directly on the instance using a WeakMap-like internal mechanism. They’re truly private: not accessible via this['#field'], not visible in Object.keys, not inherited by subclasses, and not accessible through Proxies. This is different from the convention of _privateField which is just naming convention with no enforcement. Under the hood, V8 uses a per-class brand check — when you access #field, the engine verifies the object was created by the right class constructor.35. What are JavaScript decorators and how does the TC39 proposal work?
35. What are JavaScript decorators and how does the TC39 proposal work?
experimentalDecorators (used by Angular, NestJS) follow an older proposal with different semantics. The TC39 Stage 3 proposal is different — decorator functions receive (value, context) instead of (target, propertyKey, descriptor). TypeScript 5.0+ supports the new standard decorators alongside the legacy ones. New projects should target the TC39 standard.Red flag answer: Confusing TypeScript’s experimental decorators with the TC39 proposal. Or saying “decorators aren’t in JavaScript” — they’re Stage 3 and shipping in engines. Candidates should understand the decorator pattern conceptually even if they haven’t used the syntax.Follow-up questions:Q: How do you achieve decorator-like behavior today without the decorator syntax?Higher-order functions serve as decorators: const loggedFn = withLogging(originalFn). For classes, higher-order components (React HOCs), mixins, and Object.defineProperty for method modification. The decorator syntax is syntactic sugar for these patterns. The key advantage of syntax: it’s declarative (@logged above the method) vs. imperative (wrapping functions at the bottom of the file), making the intent immediately visible at the declaration site.Q: What are auto-accessors in the decorator proposal?Auto-accessors (accessor keyword) create a getter/setter pair backed by a private field: accessor name = 'default'. This is useful because decorators can intercept accessors but not plain field assignments. accessor gives decorators a hook to run custom logic on property get/set without requiring the developer to manually write getter/setter boilerplate.36. Explain JavaScript's module systems: CommonJS, AMD, UMD, and ES Modules
36. Explain JavaScript's module systems: CommonJS, AMD, UMD, and ES Modules
require() inside if-blocks), copies values on require (not live bindings), single-threaded file resolution. Used by: Node.js (default), legacy npm packages.ES Modules (ESM) — the standard:| Feature | CommonJS | ES Modules |
|---|---|---|
| Loading | Synchronous | Asynchronous |
| Structure | Dynamic (require anywhere) | Static (top-level only, except import()) |
| Exports | Value copy | Live bindings |
| Tree-shaking | Not possible | Built-in (bundlers can eliminate dead code) |
this | module.exports | undefined |
| File extension | .js or .cjs | .mjs or .js with "type": "module" |
require and import beyond syntax. Or not understanding tree-shaking and why static imports matter for bundle optimization. Also: not knowing that ESM uses live bindings while CJS copies values.Follow-up questions:Q: What is the “dual package hazard” and how do library authors handle it?When a package ships both CJS and ESM, a consumer might end up loading both versions if different parts of the dependency tree use different module systems. This means two copies of the module in memory, and instanceof checks fail across them. Solutions: (1) Ship ESM-only (increasing trend). (2) Use package.json "exports" field with conditional exports: "exports": { "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" }. (3) Use a thin CJS wrapper that re-exports from the ESM source. The "exports" field also prevents deep imports (import x from 'pkg/internal'), which helps library authors maintain a stable public API.Q: How does import() dynamic import work for code splitting?import('module') returns a Promise that resolves to the module namespace object. Bundlers like Webpack and Vite recognize this syntax and create separate chunks. Example: const AdminPanel = lazy(() => import('./AdminPanel')) in React creates a separate JS file that’s only loaded when the admin panel is rendered. This can reduce initial bundle size by 50-70% for large apps. The chunk is fetched with a network request on first access and cached afterward. Webpack supports “magic comments” for naming chunks: import(/* webpackChunkName: "admin" */ './AdminPanel').37. What are Web Workers and how do they enable true parallelism in JavaScript?
37. What are Web Workers and how do they enable true parallelism in JavaScript?
SharedArrayBuffer and Atomics, and practical use cases.Answer:Web Workers provide true multi-threading in the browser. Each Worker runs in a separate OS-level thread with its own event loop, global scope, and JavaScript execution context. They cannot access the DOM — they communicate with the main thread via message passing.Basic Worker usage:- Figma: Runs the entire design engine in a Worker — UI stays responsive during complex operations
- OffscreenCanvas: Render graphics/charts in a Worker thread
- Encryption/hashing: Heavy crypto operations off the main thread
- WASM workloads: Run WebAssembly computation in Workers
- Video processing: Frame manipulation without UI jank
worker_threads:SharedArrayBuffer is opt-in and requires security headers.Follow-up questions:Q: What are the security requirements for SharedArrayBuffer and why?After Spectre and Meltdown (2018), browsers disabled SharedArrayBuffer because shared memory + high-resolution timing enabled side-channel attacks that could read other processes’ memory. It was re-enabled only with cross-origin isolation: the server must send Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers. These headers prevent the page from loading cross-origin resources without explicit opt-in, which mitigates the timing attack surface. performance.now() was also reduced to 1ms resolution in non-isolated contexts for the same reason.Q: How does a Worker pool improve performance over creating Workers per task?Worker creation has overhead (~50-100ms for thread creation + JS context initialization). A Worker pool pre-creates N workers and dispatches tasks via a queue. When a Worker finishes, it picks up the next task. This amortizes creation cost and limits parallelism to the number of CPU cores (creating 1000 Workers on an 8-core machine thrashes the scheduler). Libraries like workerpool and Piscina (Node.js) provide battle-tested pool implementations. The pool size should typically equal navigator.hardwareConcurrency (number of logical CPU cores).38. What are the performance implications of JavaScript closures, and how do engines optimize them?
38. What are the performance implications of JavaScript closures, and how do engines optimize them?
eval exception (optimization killer):Array.prototype.map/filter/reduce callbacks are nearly free — V8 recognizes these patterns and avoids heap allocation for the closure context.