AceDevHub

Interview questions

100 JavaScript Interview Questions and Answers (2026)

Prepare for JavaScript interviews with 100 curated questions and answers covering fundamentals, modern JavaScript, async programming, DOM, closures, prototypes, performance, and real-world scenarios.

100 questionsBeginnerIntermediateAdvanced100 Questions
1

Beginner JavaScript Interview Questions

Question 1BeginnerConcept

What is JavaScript, and where can JavaScript code run?

Direct answer

JavaScript is a high-level, dynamically typed programming language standardized as ECMAScript; it runs in browsers and in non-browser runtimes such as Node.js.

JavaScript is the programming language most closely associated with interactive web applications, but the language itself is not limited to browsers. Its standardized language specification is called ECMAScript.

  • Browsers run JavaScript through engines such as V8, SpiderMonkey, or JavaScriptCore and expose Web APIs such as the DOM, fetch, timers, and storage.
  • Server runtimes such as Node.js execute JavaScript outside the browser and provide APIs for files, networking, processes, and servers.
  • Other environments include edge runtimes, desktop shells, mobile frameworks, build tools, and embedded systems.
Question 2BeginnerConcept

What are the data types in JavaScript?

Direct answer

JavaScript has seven primitive types—string, number, bigint, boolean, undefined, symbol, and null—and one non-primitive category: object.

A useful interview answer starts by separating primitive values from objects. JavaScript currently has seven primitive types: string, number, bigint, boolean, undefined, symbol, and null. Everything else that is not a primitive is an object.

  • Primitive examples: "hello", 42, 42n, true, undefined, Symbol('id'), null
  • Object examples: plain objects, arrays, dates, maps, sets, regular expressions, and functions. Functions are callable objects.
Question 3BeginnerComparison

What is the difference between primitive values and objects in JavaScript?

Direct answer

Primitive values are immutable values copied by value, while objects are mutable structures whose variables hold references to the same underlying object.

The phrase “passed by reference” is often used loosely in interviews. A more precise explanation is that JavaScript always passes values. For objects, the value being copied is a reference to the object.

let a = 10; let b = a; b = 20; leaves a unchanged because the number value was copied. With const a = { count: 1 }; const b = a; b.count = 2;, both variables point to the same object, so reading a.count gives 2.

Question 4BeginnerConcept

What does it mean that JavaScript is dynamically typed?

Direct answer

JavaScript is dynamically typed because variable bindings do not have a fixed declared type; the type belongs to the current value and can change after reassignment.

In JavaScript you do not declare a variable as an integer, string, or boolean. A binding can point to values of different types over time. For example, let value = 10; value = 'ten'; is valid.

  • Dynamic typing makes rapid development convenient because less type syntax is required.
  • It also means some type mistakes are discovered only while code executes unless tooling such as TypeScript or runtime validation is used.
  • Dynamic typing is different from type coercion: dynamic typing is about what values a variable can hold; coercion is about converting values during an operation.
Question 5BeginnerComparison

What is the difference between var, let, and const in JavaScript?

Direct answer

var is function-scoped and can be redeclared; let and const are block-scoped, and const additionally prevents reassignment of the binding.

The important differences are scope, redeclaration, reassignment, and behavior before initialization.

  • var: function-scoped (or global at top level in classic scripts), can be redeclared, can be reassigned, and is initialized to undefined during environment setup.
  • let: block-scoped, cannot be redeclared in the same scope, can be reassigned, and cannot be accessed during its temporal dead zone.
  • const: block-scoped, must be initialized, cannot be reassigned, and also has a temporal dead zone.

A const object can still have its properties changed. const user = {name: 'A'}; user.name = 'B'; is valid because the binding still points to the same object.

Question 6BeginnerConcept

What is scope in JavaScript, and what are global, function, and block scope?

Direct answer

Scope determines where a variable or function can be accessed; JavaScript commonly uses global scope, function scope, and block scope.

Scope is the set of places in code where an identifier can be resolved. Inner scopes can normally access identifiers from their outer lexical scopes, but outer scopes cannot directly access variables declared only inside an inner scope.

  • Global scope: values declared at the top level and available broadly within the current script or module rules.
  • Function scope: bindings such as var and function-local declarations that are visible throughout a function.
  • Block scope: let, const, and class declarations restricted to blocks such as if statements, loops, and standalone braces.
Question 7BeginnerConcept

What is hoisting in JavaScript?

Direct answer

Hoisting describes how declarations are processed before execution: function declarations are initialized early, var is initialized to undefined, while let, const, and class remain uninitialized until their declaration is evaluated.

Saying “JavaScript physically moves declarations to the top” is a teaching shortcut. A more accurate model is that bindings are created during environment setup before normal statement execution begins.

  • Function declarations can generally be called before the declaration appears in source code.
  • var exists before its declaration line and initially has the value undefined.
  • let, const, and class also have bindings before the declaration line, but accessing them too early throws a ReferenceError because they are in the temporal dead zone.

console.log(count); var count = 1; logs undefined, whereas console.log(count); let count = 1; throws a ReferenceError.

Question 8BeginnerConcept

What is the Temporal Dead Zone (TDZ) in JavaScript?

Direct answer

The Temporal Dead Zone is the period after entering a scope but before a let, const, or class binding is initialized; accessing that binding during this period throws a ReferenceError.

The TDZ begins when execution enters the relevant scope and ends when the declaration is evaluated. The binding exists, but it is not yet initialized.

For example, { console.log(user); let user = 'Ada'; } throws a ReferenceError. This is different from var, which is initialized to undefined before execution reaches its declaration.

Question 9BeginnerComparison

What is the difference between null and undefined in JavaScript?

Direct answer

undefined usually represents a missing or not-yet-assigned value, while null is an explicit value commonly used to represent an intentional absence.

Both null and undefined are primitive, falsy, and nullish values, but they usually communicate different intent.

  • undefined: commonly appears for an uninitialized variable, a missing object property, or a function that returns no explicit value.
  • null: is commonly assigned intentionally to represent “no value” or “no object.” Some Web APIs also return null for a missing result.

null == undefined is true because loose equality has a special nullish rule, but null === undefined is false because they are different types.

Question 10BeginnerConcept

How does the typeof operator work in JavaScript, and what are its common quirks?

Direct answer

typeof returns a string describing the operand's type, but notable cases include typeof null === 'object' and typeof a function === 'function'.

The typeof operator is useful for quick runtime checks. Typical results include 'string', 'number', 'boolean', 'undefined', 'bigint', 'symbol', 'function', and 'object'.

  • typeof null returns 'object' because of a long-standing historical behavior.
  • typeof [] returns 'object'; use Array.isArray(value) when you specifically need to detect arrays.
  • typeof NaN returns 'number' even though NaN represents an invalid numeric result.
Question 11BeginnerConcept

What are truthy and falsy values in JavaScript?

Direct answer

Falsy values become false in a boolean context; all other values are truthy.

JavaScript automatically converts values to booleans in conditions such as if, while, and logical expressions. The small set of falsy values is worth memorizing.

  • Falsy: false, 0, -0, 0n, an empty string, null, undefined, and NaN.
  • Truthy: every other value, including empty arrays, empty objects, the string "0", and the string "false".

This means if ([]) { ... } runs because an empty array is still an object and therefore truthy.

Question 12BeginnerComparison

What is the difference between == and === in JavaScript?

Direct answer

== performs abstract equality and may coerce operand types, while === performs strict equality without type coercion.

Strict equality (===) compares without converting values to another type. Loose equality (==) follows coercion rules before or during comparison.

Examples: 42 == '42' is true, while 42 === '42' is false. Also, null == undefined is true but their strict equality is false.

  • Use === as the default because its behavior is easier to reason about.
  • Know == because interview questions often test its coercion rules and legacy code may use it.
  • Object equality with either operator normally compares object identity, not deep content.
Question 13BeginnerConcept

What is type coercion in JavaScript? Explain implicit and explicit coercion.

Direct answer

Type coercion is conversion from one type to another; it is implicit when JavaScript performs it automatically and explicit when your code requests the conversion.

An explicit conversion is visible in code, for example Number('42'), String(42), or Boolean(value). An implicit conversion happens as part of an operation.

For example, 1 + '2' produces the string '12' because the string operand causes concatenation, while '6' - 1 produces the number 5 because subtraction requires numeric operands.

Question 14BeginnerComparison

What is the difference between a function declaration and a function expression?

Direct answer

A function declaration defines a named function as a declaration and is initialized before normal execution, while a function expression creates a function as part of an expression and follows the initialization rules of its containing variable.

A declaration looks like function greet() {}. An expression can look like const greet = function () {}; or const greet = () => {};.

  • Function declarations can usually be called earlier in the same scope because the function binding is initialized during environment setup.
  • A function expression assigned to let or const cannot be used before that variable is initialized.
  • Function expressions are convenient when functions are passed around as values, assigned conditionally, or created inline.
Question 15BeginnerComparison

How are arrow functions different from regular functions in JavaScript?

Direct answer

Arrow functions use shorter syntax and capture this lexically; unlike regular functions, they do not have their own this, arguments, or constructor behavior.

Arrow functions are useful for concise callbacks, for example const double = n => n * 2;. Their biggest semantic difference is that they do not create their own this binding.

  • Arrow functions capture this from the surrounding lexical scope.
  • They do not have their own arguments object; rest parameters are usually preferred when arguments are needed.
  • They cannot be called with new as constructors.
  • Regular functions are usually better when a method needs dynamic this based on how it is called.
Question 16BeginnerConcept

What is a callback function in JavaScript?

Direct answer

A callback is a function passed to another function so the receiving code can invoke it later or as part of its operation.

Functions are first-class values in JavaScript, so they can be stored in variables, passed as arguments, and returned from other functions. A callback takes advantage of that capability.

In [1, 2, 3].map(n => n * 2), the arrow function is a callback invoked by map. Event handlers and older asynchronous APIs also commonly use callbacks.

Question 17BeginnerConcept

What is a higher-order function in JavaScript?

Direct answer

A higher-order function is a function that takes one or more functions as arguments, returns a function, or both.

Because JavaScript functions are values, functions can operate on other functions. Array methods such as map, filter, and reduce are common higher-order functions because they receive callbacks.

A function can also return another function, such as const multiplyBy = a => b => a * b;. Calling multiplyBy(2) creates a new function specialized to multiply by two.

Question 18BeginnerComparison

What is the difference between map() and forEach() in JavaScript?

Direct answer

map() creates and returns a new array from callback results, while forEach() iterates for side effects and returns undefined.

Use map() when each input element should produce a corresponding output element. Use forEach() when the goal is an action such as logging, updating an external value, or invoking another operation.

const doubled = [1,2,3].map(n => n * 2); produces [2,4,6]. By contrast, assigning the result of [1,2,3].forEach(...) gives undefined.

Question 19BeginnerComparison

When should you use map(), filter(), and reduce() in JavaScript?

Direct answer

Use map to transform items, filter to keep selected items, and reduce to combine an array into an accumulated result.

  • map() transforms each element and returns a new array of the same length.
  • filter() tests each element and returns a new array containing only elements whose callback result is truthy.
  • reduce() carries an accumulator across the array and can produce a number, string, object, array, map, or any other final value.

For const nums = [1,2,3,4];, you might use nums.map(n => n * 2), nums.filter(n => n % 2 === 0), or nums.reduce((sum, n) => sum + n, 0) depending on the desired result.

Question 20BeginnerComparison

What is the difference between spread syntax and rest syntax in JavaScript?

Direct answer

Both use ..., but spread expands values while rest collects multiple values into a single array or object pattern.

Spread appears where values are being expanded, for example const copy = [...items] or fn(...args). Rest appears where remaining values are being collected, for example function fn(...args) {}.

  • Array spread expands iterable elements into a new array or function call.
  • Object spread copies enumerable own properties into a new object.
  • Rest parameters collect remaining function arguments into an actual array.
  • Rest patterns can collect remaining properties or elements during destructuring.
Question 21BeginnerConcept

What is destructuring in JavaScript?

Direct answer

Destructuring is syntax for unpacking array elements or object properties into variables, parameters, or assignment targets.

Array destructuring is positional: const [first, second] = items;. Object destructuring is property-based: const { name, age } = user;.

  • Values can be renamed, such as const { id: userId } = user.
  • Default values can be provided when the extracted value is undefined.
  • Rest syntax can collect remaining items or properties.
  • Destructuring is commonly used in function parameters to make required fields explicit.
Question 22BeginnerConcept

What are template literals in JavaScript, and how are they different from normal strings?

Direct answer

Template literals use backticks and support interpolation, multiline text, and tagged templates.

Template literals use backticks rather than single or double quotes. Expressions can be embedded with ${...}, for example `Hello ${user.name}`.

  • They make string interpolation easier to read than repeated + concatenation.
  • They can span multiple lines without explicit newline escape sequences.
  • They can be processed by tag functions, which is a more advanced use case.
Question 23BeginnerConcept

What is optional chaining (?.) in JavaScript, and when should you use it?

Direct answer

Optional chaining safely continues property access or optional calls only when the value on the left is not null or undefined; otherwise it returns undefined.

Without optional chaining, code often contains repeated existence checks before reading nested data. user?.address?.city returns undefined if user or address is nullish instead of throwing while trying to access the next property.

  • Property access: obj?.property
  • Computed access: obj?.[key]
  • Optional call: callback?.()
Question 24BeginnerComparison

What is the difference between the nullish coalescing operator (??) and logical OR (||)?

Direct answer

?? uses the fallback only for null or undefined, while || uses the fallback for any falsy left-hand value.

This difference matters when 0, false, or an empty string is a valid value. For example, 0 || 10 evaluates to 10, while 0 ?? 10 evaluates to 0.

A common pattern is user?.settings?.pageSize ?? 20: optional chaining safely reads the property, then nullish coalescing provides a default only when the property is actually missing/nullish.

Question 25BeginnerComparison

What is the difference between a shallow copy and a deep copy in JavaScript?

Direct answer

A shallow copy duplicates only the outer container while nested object references remain shared; a deep copy recursively creates independent nested values where cloning is supported.

Array spread, object spread, Array.from(), and common uses of Object.assign() create shallow copies. That means top-level properties are copied, but nested objects can still point to the same object.

With const a = { profile: { name: 'A' } }; const b = { ...a };, changing b.profile.name also affects what a.profile.name reads because profile is shared.

  • Use a shallow copy when nested sharing is acceptable or nested values are treated immutably.
  • For supported data, structuredClone() can create a deep clone of many built-in types.
  • JSON stringify/parse is not a general-purpose deep clone because it loses or rejects several JavaScript value types.
Question 26BeginnerConcept

What are JavaScript modules, and how do import and export work?

Direct answer

JavaScript modules split code into files with explicit exports and imports, giving each module its own scope and a dependency graph managed by the runtime or tooling.

An ES module can expose values with export and consume them with import. For example, export const add = (a,b) => a+b; can be consumed using import { add } from './math.js';.

  • Named exports allow multiple explicitly named values.
  • A default export provides one default binding per module, imported with a locally chosen name.
  • Module imports are statically analyzable, which helps tools understand dependencies.
  • Modules have their own top-level scope rather than sharing ordinary script-level declarations.
Question 27BeginnerComparison

What is the difference between synchronous and asynchronous JavaScript?

Direct answer

Synchronous code completes in sequence on the current call stack, while asynchronous workflows allow work to finish later and schedule continuation code without blocking the entire program for that operation.

JavaScript execution on a given main thread runs one piece of JavaScript at a time, but host environments provide asynchronous capabilities such as timers, network requests, and events.

  • Synchronous: the next statement waits until the current JavaScript work finishes.
  • Asynchronous: an operation can be initiated now and its continuation handled later through callbacks, promises, events, or async/await.
Question 28BeginnerConcept

What is a Promise in JavaScript, and what are its states?

Direct answer

A Promise represents the eventual completion or failure of an asynchronous result and is pending before becoming fulfilled or rejected.

A Promise starts in the pending state and later becomes either fulfilled with a value or rejected with a reason. Once settled, that state does not change.

  • then() registers fulfillment handling and can also receive a rejection handler.
  • catch() is commonly used for rejection handling.
  • finally() runs cleanup-style logic after settlement regardless of success or failure.

Promise chaining works because then() returns a new Promise. Returning a normal value feeds the next step; returning a Promise makes the chain wait for it; throwing causes the next rejection path.

Question 29BeginnerConcept

What are async and await in JavaScript?

Direct answer

async/await is Promise-based syntax that lets asynchronous control flow be written in a sequential style; an async function always returns a Promise.

Marking a function async makes its return value become a Promise result. Inside an async function, await pauses that function's continuation until the awaited value is resolved, without blocking the JavaScript thread itself.

For example, const response = await fetch(url); waits within the async function for the fetch promise to settle before continuing to the next statement.

  • Use try/catch when you want conventional-looking error handling around awaited operations.
  • Independent asynchronous operations should not always be awaited one after another; doing so can accidentally serialize work.
  • async/await does not replace Promises—it is built on Promise semantics.
Question 30BeginnerConcept

What is the DOM, and how does JavaScript interact with a web page through it?

Direct answer

The DOM is the browser's object representation of an HTML document; JavaScript uses DOM APIs to query, create, update, and listen to events on document nodes.

The Document Object Model (DOM) is a Web API, not part of the core ECMAScript language. The browser parses HTML into a tree of objects that scripts can work with.

  • Query: document.querySelector() returns the first matching element; querySelectorAll() returns all matches in a static NodeList.
  • Update: properties such as textContent, classList, attributes, and styles can change the rendered document.
  • Create/remove: createElement(), append(), remove(), and related APIs modify the tree.
  • Events: addEventListener() connects user or browser events to JavaScript handlers.
Question 31BeginnerPractical

How would you reverse a string in JavaScript?

Direct answer

For a basic interview solution, convert the string to an array, reverse it, and join it; for Unicode-heavy text, discuss the limitations of naive character splitting.

A concise baseline solution is const reverseString = str => [...str].reverse().join('');. The spread creates an array of string iterator values, reverse() reverses that array, and join('') rebuilds the string.

An interviewer may also ask for a loop-based solution to test indexing and control flow. For example, build a result string from the end of the input toward the beginning.

Question 32BeginnerPractical

How can you remove duplicate values from an array in JavaScript?

Direct answer

For primitive values using SameValueZero-style equality, the common concise solution is [...new Set(array)].

A Set stores unique values, so const unique = [...new Set(items)]; is a clean solution for many arrays of primitives.

For objects, two separately created objects are different identities even if their properties look identical. If duplicates should be determined by a field such as id, use explicit key-based logic, often with a Map or Set of seen keys.

2

Intermediate JavaScript Interview Questions

Question 33IntermediateConcept

What is a closure in JavaScript, and why can an inner function access variables after the outer function returns?

Direct answer

A closure is a function together with access to the lexical environment in which it was created, so referenced outer bindings can remain reachable even after the outer function has finished executing.

A closure is not simply “a function inside another function.” The important idea is lexical scope combined with function creation. When a function is created, its scope is determined by where that function appears in source code, not by where it is later called.

For example, function makeCounter(){ let count = 0; return () => ++count; } returns a function that still refers to the count binding. Calling the returned function later can continue updating that same binding even though makeCounter() has already returned.

  • Encapsulation — keep state private behind returned functions.
  • Function factories — create callbacks configured with different captured values.
  • Async/event handlers — callbacks can retain request IDs, configuration, or local state needed later.
  • Memoization and caching — a returned function can retain a cache without exposing it globally.
Question 34IntermediateCode Output

Why do callbacks created in a loop behave differently with var and let?

Direct answer

var creates one function-scoped loop binding shared by the callbacks, while let creates a fresh per-iteration binding, so delayed callbacks capture different values.

var-loop-closure.js
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
Output
3
3
3
let-loop-closure.js
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
Output
0
1
2

Consider for (var i=0;i<3;i++){ setTimeout(() => console.log(i),0); }. By the time the callbacks run, the loop has completed and the single shared i binding contains 3, so the callbacks log 3, 3, 3.

With for (let i=0;i<3;i++){ setTimeout(() => console.log(i),0); }, each iteration gets its own lexical binding. The callbacks therefore observe 0, 1, 2. The difference is about bindings and closures; it is not that setTimeout “copies” the value.

  • With modern JavaScript, prefer let when the loop variable should be captured per iteration.
  • Before block-scoped bindings were available, developers commonly used an IIFE or another function call to create a new scope for each iteration.
  • The timer delay is not the core issue; any callback that executes later can reveal the same closure behavior.
Question 35IntermediateConcept

How is the value of this determined in a regular JavaScript function?

Direct answer

For a regular function, this is primarily determined by how the function is called: as a method, with call/apply/bind, with new, or as a plain function; arrow functions are different because they capture this lexically.

A useful interview model is to inspect the call site. The same function object can receive different this values when invoked in different ways.

  • Method call: obj.run() usually calls run with this === obj.
  • Explicit binding: fn.call(obj) and fn.apply(obj) invoke immediately with the chosen receiver; fn.bind(obj) creates a bound function.
  • Constructor call: new Fn() creates a new object and calls the constructor with that object as this unless construction semantics return another object.
  • Plain call: in strict mode, fn() receives undefined as this. Non-strict scripts can substitute the global object.

The key distinction is that this is not normally decided by where a regular function was declared. That lexical rule belongs to arrow functions.

Question 36IntermediateComparison

Why do arrow functions have lexical this, and when should you avoid using an arrow function?

Direct answer

An arrow function does not create its own this binding; references to this are resolved from the surrounding scope, which is useful for callbacks but often wrong for object methods that need a dynamic receiver.

An arrow function captures the surrounding this rather than receiving one from the call site. That makes arrows convenient inside methods when an inner callback should keep the outer method receiver.

For example, a regular method can use an arrow callback: const timer = { value: 0, start(){ setTimeout(() => { this.value++; }, 100); } };. The arrow callback reuses the this of start().

  • Avoid an arrow as an object method when you expect this to become the object through obj.method().
  • Arrow functions cannot be used as constructors with new and do not have their own arguments object.
  • Using call(), apply(), or bind() cannot replace an arrow function’s lexical this with a new receiver.
Question 37IntermediateComparison

What is the difference between call(), apply(), and bind() in JavaScript?

Direct answer

call and apply invoke a function immediately with an explicit this value; call receives arguments individually, apply receives an array-like argument list, while bind returns a new function with this and optional leading arguments fixed.

call() and apply() are immediate invocation tools. bind() is a function-creation tool.

  • fn.call(user, 1, 2) → execute now with this === user and positional arguments.
  • fn.apply(user, [1, 2]) → execute now with the arguments supplied as an array-like value.
  • const bound = fn.bind(user, 1) → return a new function; calling bound(2) later supplies the remaining argument.

Modern spread syntax means apply() is less necessary merely to expand an array: fn(...args) is usually clearer when no explicit receiver is needed.

Question 38IntermediateCode Output

What happens to this when a method is detached from its object?

Direct answer

Detaching a regular method removes the object-method call site; when the detached function is called plainly, it no longer receives the original object as this.

detached-method-this.js
"use strict";

const user = { name: "Ada", getName() { return this.name; } };
const getName = user.getName;

console.log(user.getName());
getName();
Output
Ada
TypeError: Cannot read properties of undefined (reading 'name')

Given const user={name:'Ada', getName(){ return this.name; }}; const getName=user.getName;, the expressions user.getName() and getName() have different call sites.

user.getName() uses user as the receiver and returns 'Ada'. In strict-mode code, a plain getName() call gives the function undefined as this, so trying to read this.name throws. Behavior involving a global object in non-strict classic scripts should not be relied on.

  • Preserve the receiver with const getName = user.getName.bind(user);.
  • Or wrap the invocation: const getName = () => user.getName();.
  • This issue commonly appears when methods are passed as callbacks without binding.
Question 39IntermediateConcept

How does the prototype chain work in JavaScript?

Direct answer

When a property is not found as an own property, JavaScript follows the object’s internal [[Prototype]] link and repeats the lookup until it finds the property or reaches null.

JavaScript inheritance is based on delegation through prototypes. If obj.x is requested and x is not an own property of obj, lookup continues on Object.getPrototypeOf(obj) and then on that object’s prototype, continuing until null.

Methods such as array methods usually illustrate this well: an array instance does not contain its own copy of map. The method is found through Array.prototype in its prototype chain.

  • Object.hasOwn(obj, key) checks whether the property is owned directly by the object.
  • key in obj checks both own and inherited properties.
  • Assigning an own property with the same name can shadow an inherited property without modifying the prototype.
Question 41IntermediateConcept

What does the new operator do when you call a constructor in JavaScript?

Direct answer

new creates an object linked to the constructor’s prototype, calls the constructor with that object as this, and normally returns the new object unless the constructor explicitly returns another object.

A simplified mental model for new User("Ada") is: create a fresh object, connect its internal prototype to User.prototype, invoke User with the new object as this, then return the appropriate construction result.

  • If the constructor returns no value or returns a primitive, the newly created instance is returned.
  • If the constructor explicitly returns an object (including a function object), that object can become the result instead of the automatically created instance.
  • The prototype connection is why instances can access methods placed on User.prototype without storing separate method copies on each instance.

This is also why calling a traditional constructor without new can behave very differently: it becomes an ordinary function call rather than construction. JavaScript class constructors prevent that mistake by requiring construction.

Question 42IntermediateComparison

How are JavaScript classes related to constructor functions and prototypes?

Direct answer

JavaScript classes provide structured syntax and additional semantics on top of the language’s prototype-based object model; instance methods still live on the class prototype and inheritance still uses prototype links.

It is fair to say classes are built on the prototype model, but “classes are only syntax sugar” can hide meaningful behavioral rules. A class declaration creates a constructor and a prototype object, and methods declared in the class body are placed on that prototype.

  • Class constructors must be called with new; calling them as ordinary functions throws.
  • Class bodies run in strict mode.
  • Class methods are non-enumerable by default, unlike methods assigned with simple property assignment to a prototype.
  • extends and super provide standardized inheritance and parent-constructor behavior.
  • Private fields such as #count are language-enforced private state, not prototype properties.

For interview reasoning, treat class User {} as a cleaner way to define constructor/prototype relationships while remembering that classes also impose their own syntax and runtime rules.

Question 43IntermediateConcept

What does strict mode change in JavaScript, and when is it enabled automatically?

Direct answer

Strict mode opts code into stricter JavaScript semantics, preventing some error-prone behavior; ES modules and class bodies are strict automatically, while classic script/function code can use the "use strict" directive.

Strict mode was introduced to make several historically permissive behaviors fail clearly instead of silently creating surprising results. In classic scripts, it can be enabled with a directive such as 'use strict';.

  • Assigning to an undeclared identifier no longer creates an accidental global; it throws a ReferenceError.
  • In a plain function call, this remains undefined instead of being substituted with the global object.
  • Several silent assignment failures become errors, which makes bugs easier to detect.
  • The with statement is not allowed, and some legacy syntax/parameter patterns are restricted.

Modern application code often uses strict semantics without an explicit directive because type="module" scripts and imported ES modules are strict by definition, as are class bodies.

Question 44IntermediateConcept

What is an IIFE in JavaScript, and why was it historically common?

Direct answer

An IIFE is a function expression invoked immediately after it is created; it was widely used to create private function scope and avoid global variables before block scope and ES modules became standard.

A typical form is (function(){ const privateValue = 1; })(); or the arrow equivalent (() => { /* setup */ })();. The surrounding parentheses force the parser to treat the function as an expression, and the final parentheses invoke it.

  • Older browser code used IIFEs to isolate variables because var is function-scoped and script files otherwise shared globals.
  • The module pattern used IIFEs plus closures to expose a small public API while keeping implementation details private.
  • Today, let, const, block scope, and ES modules remove many historical reasons for IIFEs.
  • Async IIFEs can still be useful when an immediate async execution wrapper is convenient in a context that cannot use top-level await.
Question 45IntermediateConcept

How does the JavaScript event loop coordinate synchronous code, tasks, and microtasks?

Direct answer

JavaScript executes the current job synchronously on the call stack; the host event loop schedules tasks, and microtask checkpoints run queued microtasks such as Promise reactions before the event loop proceeds to later tasks.

The phrase “JavaScript is single-threaded” is useful only as a starting point. ECMAScript defines execution and Promise jobs, while browsers and other hosts define event loops, timers, networking, rendering, and other scheduling behavior.

  • Call stack / current execution — ordinary statements and function calls run to completion unless they throw or suspend through language/host mechanisms.
  • Tasks — browser events, timers, and other host work can queue tasks for later event-loop turns.
  • Microtasks — Promise reactions and queueMicrotask() callbacks are processed at microtask checkpoints; newly queued microtasks can extend that checkpoint.
  • Rendering — in browsers, rendering opportunities are controlled by the HTML event loop and do not simply occur after every line of JavaScript.

A zero-millisecond timer therefore means “eligible after at least the timer delay and when scheduling permits,” not “execute immediately after this statement.”

Question 46IntermediateCode Output

What is the output order when synchronous code, Promise callbacks, and setTimeout(0) are mixed?

Direct answer

Synchronous logs run first, then queued Promise microtasks, then the timer task in a later event-loop turn.

event-loop-order.js
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
Output
A
D
C
B

For console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D');, the common browser/host ordering is A, D, C, B.

  • A prints during the current synchronous execution.
  • The timer callback is scheduled for a future task.
  • Promise.then() schedules a Promise reaction job that the host processes as a microtask.
  • D prints before the current stack finishes.
  • At the microtask checkpoint, C runs before the event loop takes the timer task that prints B.

A stronger interview follow-up is to add another Promise.then() inside the first microtask. That new microtask is normally processed in the same checkpoint before the event loop advances to the timer task.

Question 47IntermediateConcept

How does Promise chaining propagate returned values, returned Promises, and thrown errors?

Direct answer

Each then/catch call returns a new Promise: a returned normal value fulfills the next Promise, a returned Promise/thenable is adopted, and a thrown exception rejects the next Promise.

Promise chains are composable because then() does not mutate and return the same Promise. It creates a new Promise whose outcome depends on the handler result.

  • return 42 from a fulfillment handler → the next then receives 42.
  • return fetch(url) → the chain waits for that returned Promise instead of nesting a Promise as a plain value.
  • throw new Error('x') → the new Promise is rejected and control jumps to the next matching rejection handler.
  • If a handler is omitted, fulfillment values or rejection reasons pass through to later links.

A common bug is starting asynchronous work inside then() without returning it. The outer chain then cannot wait for that work or reliably propagate its failure.

Question 48IntermediateComparison

When should you use Promise.all(), Promise.allSettled(), Promise.any(), or Promise.race()?

Direct answer

Use Promise.all when all results are required, allSettled when every outcome matters, any when the first fulfillment is enough, and race when the first settlement—fulfillment or rejection—should decide the result.

  • Promise.all() fulfills with results in input order when all inputs fulfill; it rejects when an input rejects. That rejection does not automatically cancel the remaining operations.
  • Promise.allSettled() waits for every input and fulfills with status records, making it useful for independent work where partial failure is expected.
  • Promise.any() fulfills with the first successful value; it rejects with an AggregateError only when all inputs reject.
  • Promise.race() settles as soon as the first input settles, whether that first result is fulfillment or rejection.

These methods accept iterables of values/Promises and return a Promise. They coordinate outcomes; they do not themselves make CPU-bound JavaScript execute on multiple CPU cores.

A dashboard loading independent widgets might prefer allSettled. A request requiring user, permissions, and configuration together might prefer all. Multiple equivalent mirrors can be a fit for any.

Question 49IntermediatePractical

How do you avoid accidentally running independent asynchronous operations sequentially with await?

Direct answer

If operations are independent, start them before awaiting or coordinate them with a Promise combinator; awaiting each operation before starting the next creates sequential latency.

This code is sequential: const a = await fetchA(); const b = await fetchB();. fetchB() is not even started until fetchA() has completed.

For independent work, you can start both first: const pa = fetchA(); const pb = fetchB(); const [a,b] = await Promise.all([pa,pb]);. Total latency can then approach the slower operation rather than the sum of both latencies.

  • Use sequential awaits when the next operation genuinely depends on the previous result.
  • Use concurrent coordination for independent operations when the downstream system can safely handle the concurrency.
  • For hundreds or thousands of operations, unlimited Promise.all() may overload APIs, databases, memory, or rate limits; use bounded concurrency instead.
Question 50IntermediateScenario

How should errors be handled in Promise chains and async/await code?

Direct answer

Handle an error at the layer that can add context or recover, allow unrecoverable errors to propagate, and use finally for cleanup that must run on both success and failure.

An async function converts an uncaught thrown exception into a rejected Promise. That means try/catch around an await and .catch() on a Promise chain are two ways of participating in the same rejection model.

  • Catch when you can recover, translate the error, add useful context, or produce a deliberate fallback.
  • Do not write empty catches that silently convert failures into undefined behavior.
  • If you catch only to log, consider whether you also need to rethrow so upstream code still sees failure.
  • finally is useful for releasing UI/loading state or other cleanup; it should not normally replace the original result unless it throws or returns a rejected Promise.

With concurrent operations, the combinator matters: Promise.all() exposes the first observed rejection for the aggregate, whereas Promise.allSettled() lets you inspect every outcome.

Question 51IntermediatePractical

How does AbortController help cancel asynchronous browser operations such as fetch?

Direct answer

AbortController exposes an AbortSignal that supported APIs can observe; calling controller.abort() marks the signal aborted and lets those APIs terminate or reject their work cooperatively.

A common pattern is const controller = new AbortController(); fetch(url,{signal:controller.signal}); and later controller.abort(). The API receiving the signal decides how to react to cancellation.

  • Use cancellation when a component unmounts, a newer search supersedes an older request, or the user explicitly cancels an operation.
  • An AbortSignal stays aborted; do not try to “reset” and reuse the same controller for a new independent operation.
  • Multiple operations can share a signal when they should be cancelled as one group.
  • Aborting a client request does not guarantee that a server-side mutation was never started or completed. Application-level idempotency and transaction semantics are separate concerns.

Cancellation also prevents stale responses from racing with newer UI state, but request identity checks can still be useful when multiple operations are allowed to finish.

Question 52IntermediateComparison

What is the difference between event capturing and event bubbling in the DOM?

Direct answer

Capturing travels from ancestors toward the target before target handling, while bubbling travels from the target back through ancestors; most event listeners participate in the bubbling phase by default.

DOM event dispatch has propagation phases. For a click on a nested button, ancestors can observe the event on the way toward the target during capture and again on the way outward during bubbling if listeners were registered for those phases.

  • Register a capture listener with addEventListener('click', handler, {capture:true}) (or the boolean capture form).
  • Default listeners generally observe the target/bubbling path.
  • Not every event type bubbles, so event-delegation strategies must account for the actual event being used.
  • Propagation order is separate from the browser’s default action, such as following a link or submitting a form.

Capturing can be useful for interception/observation high in a tree; bubbling is the foundation of common event-delegation patterns.

Question 53IntermediatePractical

What is event delegation, and what is the difference between event.target and event.currentTarget?

Direct answer

Event delegation attaches a listener to an ancestor and handles bubbled events from descendants; event.target is where the event originated, while event.currentTarget is the element whose listener is currently running.

Instead of adding one click listener to every row in a dynamic list, attach one listener to the list container and determine which descendant was activated. This works because suitable events bubble through ancestors.

In a delegated listener, event.currentTarget is normally the parent/container where the listener was registered. event.target may be the deepest clicked element—for example an icon inside a button rather than the button itself.

  • Use event.target.closest('[data-action]') when nested markup can exist inside the actionable element.
  • Verify the matched element belongs to the intended delegation container if DOM structure can cross boundaries.
  • Delegation naturally covers matching descendants added after the listener is registered, reducing listener-management work.
Question 54IntermediateComparison

What is the difference between preventDefault(), stopPropagation(), and stopImmediatePropagation()?

Direct answer

preventDefault requests cancellation of the event’s default browser action, stopPropagation stops further propagation through ancestors/descendants, and stopImmediatePropagation also prevents later listeners on the same target from running.

  • preventDefault() — affects the default action when the event is cancelable, such as navigation or form submission; it does not inherently stop propagation.
  • stopPropagation() — stops the event from continuing through the propagation path, but other listeners already registered on the same current target may still run.
  • stopImmediatePropagation() — stops propagation and prevents subsequent listeners on the same target from executing for that event.

These methods solve different problems, so using stopPropagation() everywhere to “fix” event bugs can break analytics, delegation, overlays, or other components that legitimately observe the event.

Also remember that some listeners can be registered as passive, in which case the browser does not allow that listener to cancel the default action with preventDefault().

Question 55IntermediateComparison

What is the difference between script async, script defer, and type="module" in the browser?

Direct answer

For classic external scripts, async executes as soon as the download is ready without preserving document order, defer waits until parsing finishes and preserves deferred-script order; module scripts are deferred by default unless async is specified.

Without special attributes, a classic external script encountered during HTML parsing can block further parsing while it is fetched/executed. async and defer let downloading overlap with HTML parsing but differ in execution timing and ordering.

  • async — execute as soon as ready; useful for independent scripts where ordering relative to other scripts/DOM parsing is not required.
  • defer — execute after parsing completes and before DOMContentLoaded; deferred classic scripts preserve document order.
  • type="module" — module scripts are deferred by default, have module scope, use strict semantics, and can use import/export; async changes the default scheduling behavior for external modules.
Question 56IntermediateComparison

What is the difference between cookies, localStorage, and sessionStorage?

Direct answer

Cookies are HTTP state that can be sent with matching requests and can use security attributes; localStorage and sessionStorage are synchronous origin-scoped browser storage APIs for string data, with different lifetimes and tab/session behavior.

  • Cookies — small HTTP-oriented state; matching cookies may be attached to requests. Attributes such as HttpOnly, Secure, and SameSite materially affect security and delivery.
  • localStorage — persists for the origin until cleared/evicted according to browser behavior; JavaScript reads/writes string keys synchronously.
  • sessionStorage — scoped to an origin within a particular top-level browsing context/session and normally cleared when that page session ends.

Because Web Storage is synchronous, large or frequent serialization can block the main thread. For larger structured client-side data, IndexedDB is usually a more appropriate browser storage mechanism.

Security is not simply “cookies bad, localStorage good” or the reverse. XSS can read script-accessible storage, while HttpOnly cookies prevent JavaScript access but require CSRF/session design considerations.

Question 57IntermediateConcept

What are important limitations of JSON.stringify() and JSON.parse()?

Direct answer

JSON serialization supports JSON-compatible data, not arbitrary JavaScript object semantics; some values are omitted or transformed, BigInt and cycles need special handling, and prototypes/methods are not reconstructed by JSON.parse().

JSON is a data interchange format. Treating JSON.parse(JSON.stringify(value)) as a universal object clone loses information because JavaScript contains many values that JSON cannot represent directly.

  • undefined, functions, and symbols are not represented as normal object property values in the resulting JSON.
  • NaN and infinities serialize as null.
  • Date typically becomes a string through its JSON conversion behavior rather than remaining a Date object after parsing.
  • Map and Set do not automatically become their logical entries without custom conversion.
  • Cyclic object graphs throw unless you design a replacer/serialization scheme that handles references.
  • BigInt is not directly JSON-serializable without custom handling.

Use replacer/reviver functions or an application-specific schema when values need controlled conversion. For data interchange, explicit schemas are often more reliable than hoping arbitrary runtime objects round-trip.

Question 58IntermediateComparison

How is structuredClone() different from JSON.parse(JSON.stringify(...)) for deep copying?

Direct answer

structuredClone uses the structured clone algorithm and supports cyclic graphs and many built-in data types that JSON loses, but it still cannot clone every JavaScript value and should not be treated as a way to preserve arbitrary executable object behavior.

structuredClone(value) is usually a much better built-in choice than a JSON round-trip when the goal is to duplicate supported in-memory data.

  • It can preserve cycles and supports many structured data types such as Date, Map, Set, typed arrays, and ArrayBuffer.
  • Functions and DOM nodes are not general cloneable data values and can cause cloning to fail.
  • Structured cloning is about data, not preserving arbitrary class behavior, accessors, property descriptors, or every custom prototype relationship exactly as authored.
  • Transferable objects can sometimes be transferred rather than copied when using APIs/options that support transfer lists.

For immutable application state, you often do not need to deep-clone an entire graph. Copying only the path being changed is usually cheaper and preserves structural sharing.

Question 59IntermediateConcept

Why does === not perform deep equality for objects, and what makes deep equality difficult?

Direct answer

For objects, strict equality compares identity: two references are equal only when they refer to the same object; deep equality requires an explicit policy for recursively comparing structure and special value types.

{} === {} is false because each object literal creates a distinct object. In contrast, const a={}; const b=a; a===b is true because both variables contain the same object reference.

  • A deep comparison must decide whether property order matters and whether inherited or only own properties count.
  • It needs rules for arrays, Date, RegExp, Map, Set, typed arrays, and other built-ins.
  • Cyclic graphs require tracking already-compared object pairs to avoid infinite recursion.
  • Functions and custom class instances raise semantic questions: identity, source text, prototype, or domain-specific equality?

Object.is() is another identity/value comparison primitive with notable differences from === for NaN and signed zero; it is still not deep equality.

Question 60IntermediateComparison

What is the difference between Object.freeze(), Object.seal(), and Object.preventExtensions()?

Direct answer

preventExtensions blocks new own properties, seal additionally makes existing properties non-configurable, and freeze additionally makes existing data properties non-writable; all three are shallow operations.

  • Object.preventExtensions(obj) — no new own properties can be added, but existing configurable/writable properties can still be changed or removed according to their descriptors.
  • Object.seal(obj) — prevents extensions and makes existing own properties non-configurable, so they cannot normally be deleted or reconfigured; writable data properties can still change value.
  • Object.freeze(obj) — seals the object and makes existing data properties non-writable, providing the strongest of these three shallow restrictions.

“Shallow” matters: Object.freeze({settings:{theme:"dark"}}) does not automatically freeze the nested settings object. A deep-freeze utility must traverse the graph and handle cycles deliberately.

Freezing also does not mean all observable state reachable from the object can never change; accessor functions or referenced objects can still encapsulate mutable state.

Question 61IntermediateConcept

What are JavaScript property descriptors, and what do writable, enumerable, and configurable control?

Direct answer

Property descriptors define how an own property behaves: data properties can control value and writability, accessors use get/set functions, and enumerable/configurable determine visibility in common enumeration and whether the property can be reconfigured or deleted.

Use Object.getOwnPropertyDescriptor(obj, key) to inspect a property and Object.defineProperty() to define one with explicit descriptor flags.

  • writable — for data properties, controls whether assignment can change the stored value.
  • enumerable — affects whether the property appears in operations such as Object.keys() and normal own-property enumeration.
  • configurable — controls deletion and most future descriptor reconfiguration.
  • get/set — accessor properties compute reads/writes rather than storing a normal value field.

A subtle interview detail: properties created with ordinary assignment are normally writable, enumerable, and configurable, whereas flags omitted in a new descriptor passed to Object.defineProperty() default to false.

Question 62IntermediateComparison

When should you use Map instead of a plain Object in JavaScript?

Direct answer

Use Map for a dynamic key-value collection—especially when keys are not just strings/symbols, frequent iteration is central, or explicit size/collection APIs help; use plain objects for record-like structured data with known property names.

  • Map accepts keys of any value type, exposes size, has direct get/set/has/delete methods, and is directly iterable in insertion order.
  • Object uses string and symbol property keys and naturally models structured records that interact with object syntax, destructuring, JSON-oriented data, and prototypes.
  • A normal object inherits from a prototype, so arbitrary dictionary keys can collide with inherited names unless you check own properties or use Object.create(null).
  • Do not claim Map is always faster. Performance depends on workload and engine; choose based on semantics first and measure hot paths.

A user profile with fixed fields such as {id,name,email} is naturally an object. A cache keyed by request objects or IDs with frequent insert/delete/iteration may be a better Map.

Question 63IntermediateComparison

When is Set a better choice than an Array, and how does Set determine uniqueness?

Direct answer

Set is designed for unique membership and uses SameValueZero-style value comparison; arrays are ordered sequences that allow duplicates and provide richer index-based operations.

A Set stores each value at most once and preserves insertion order for iteration. It is often a clearer semantic choice when the operation is “is this value a member?” rather than “what value is at index i?”

  • Primitive duplicates collapse as expected; NaN is treated as the same Set value as NaN, and positive/negative zero are not distinct Set members.
  • Objects are unique by identity, so two different {} objects remain two Set entries even if they have the same visible properties.
  • Arrays are better when duplicates matter, numeric indexing matters, or array transformation APIs and positional semantics dominate.
  • Converting [...new Set(array)] is convenient for primitive/reference-identity deduplication but is not deep deduplication of structurally equal objects.
Question 64IntermediateConcept

What are WeakMap and WeakSet, and why are they useful for object-associated metadata?

Direct answer

Weak collections hold their keys/items weakly, so their presence does not by itself keep otherwise unreachable keys alive; this makes them useful for metadata and caches tied to object lifetime, and they intentionally do not expose normal enumeration.

A common use case is associating metadata with DOM nodes or application objects without forcing those objects to stay alive solely because the metadata table still exists.

  • WeakMap associates garbage-collectable keys with arbitrary values.
  • WeakSet tracks garbage-collectable values without mapping each one to another value.
  • Weak collections do not provide normal iteration or a reliable size because exposing liveness would make garbage-collection timing observable in problematic ways.
  • If you need to enumerate every entry, a normal Map or Set is the appropriate structure.

For most interviews, “WeakMap keys are objects” is the familiar model. Modern ECMAScript also permits certain non-registered Symbols as weakly held keys; the core concept is that the key must be garbage-collectable.

Question 65IntermediateComparison

What is the difference between an iterable and an iterator in JavaScript?

Direct answer

An iterable has a Symbol.iterator method that produces an iterator; an iterator has a next() method that returns objects containing value and done, allowing consumers such as for...of and spread to pull values sequentially.

Arrays, strings, Maps, and Sets are built-in iterables. That is why operations such as for (const x of value) and [...value] can consume them.

  • Iterable → implements [Symbol.iterator]().
  • Iterator → implements next() and returns results such as {value: 10, done: false}.
  • An iterator can also be iterable by returning itself from [Symbol.iterator](), which makes it compatible with iterable-consuming syntax.
  • Plain object literals are not automatically iterable merely because they have properties; use Object.keys/values/entries or define an iteration protocol deliberately.

The protocol enables lazy consumption: the producer does not need to create an entire result array before the consumer starts reading values.

Question 66IntermediateConcept

What are generator functions, and how do yield and next() work?

Direct answer

A generator function declared with function* returns a generator object; execution can pause at yield and later resume when next() is called, making generators useful for lazy iteration and controllable sequences.

Calling a generator function does not execute its body to completion immediately. It returns a generator object. The first next() begins execution until a yield (or return/end), producing an iterator result.

For function* ids(){ yield 1; yield 2; }, repeated next() calls produce values 1 and 2, then a result with done: true.

  • Generators work naturally with for...of and spread because generator objects implement the iteration protocols.
  • A value passed to a later next(value) can become the result of the suspended yield expression inside the generator.
  • return() and throw() allow consumers to influence generator completion/error flow.
  • Use generators when laziness or pull-based iteration clarifies the model; do not replace simple array transformations with generators purely for cleverness.
Question 67IntermediateComparison

What is the difference between currying and partial application in JavaScript?

Direct answer

Currying transforms a multi-argument function into a sequence of single-argument functions, while partial application fixes some arguments now and returns a function for the remaining arguments without requiring one argument per level.

If add(a,b,c) becomes add(a)(b)(c), that is currying. If add(1,2,3) becomes a new function such as addOne = (...rest) => add(1, ...rest), that is partial application.

  • Currying can make function composition and staged configuration expressive.
  • Partial application is often simpler when you only want to preconfigure a few leading/context arguments.
  • Function.prototype.bind() can provide a form of partial application for leading arguments in addition to binding this.
  • JavaScript functions are not automatically curried; currying is a transformation/pattern you implement or obtain from a library.

A practical example is creating a configured validator or logger once and reusing the returned function rather than passing the same configuration on every call.

Question 68IntermediateComparison

What is the difference between debouncing and throttling, and when would you use each?

Direct answer

Debouncing waits for a quiet period before invoking a function, while throttling limits execution to at most a controlled rate during sustained activity; search input often favors debounce, while continuous scroll/resize work often favors throttle.

Both patterns reduce expensive work triggered by high-frequency events, but their timing semantics differ.

  • Debounce — reset the delay whenever a new event arrives; trailing execution occurs after activity stops. Useful for search suggestions, validation, autosave batching, and resize-end behavior.
  • Throttle — allow execution at controlled intervals while activity continues. Useful for progress/scroll position, drag tracking, or telemetry that should update during the interaction without firing for every event.

A production-quality utility must make policy choices: leading vs trailing calls, preserving this and arguments, a cancel method, optional flush behavior, and what result repeated calls should observe.

For visual updates, requestAnimationFrame can sometimes be a better scheduling primitive than a generic timer throttle because it aligns work with browser rendering opportunities.

Question 69IntermediateScenario

What is memoization, and what pitfalls should you consider when memoizing JavaScript functions?

Direct answer

Memoization caches a function result for previously seen inputs, which can save repeated expensive work when the function is deterministic enough, but cache-key correctness, memory growth, invalidation, and object identity must be designed deliberately.

A simplistic memoizer that stores cache[arg] works only for a narrow input domain. Real functions may accept multiple arguments, objects, symbols, or values whose string representations collide.

  • Correctness — memoizing an impure function or one that depends on time, global state, locale, permissions, or mutable inputs can return stale/wrong results.
  • Key design — object arguments may need identity-based Map/WeakMap structures or a stable domain-specific key.
  • Memory — an unbounded cache can become a memory leak; consider LRU, TTL, size limits, or weak keys depending on the use case.
  • Cost — hashing/serialization and cache lookups can cost more than recomputing cheap functions.

Memoization is most attractive when repeated calls with equivalent inputs are common and the saved computation is meaningfully more expensive than cache maintenance.

Question 70IntermediateScenario

What commonly causes memory leaks in browser JavaScript, and how would you investigate them?

Direct answer

JavaScript leaks usually happen when data that is no longer useful remains strongly reachable through listeners, timers, closures, caches, DOM references, or global structures; garbage collection cannot reclaim objects that are still reachable.

Garbage collection can handle unreachable cyclic structures, so “circular references always leak” is outdated as a general rule. The practical question is whether an unwanted object remains reachable from a live root.

  • Event listeners — long-lived targets retaining handlers that close over component/state data.
  • Timers/subscriptions — intervals, observers, sockets, or custom subscriptions that are never cancelled/unsubscribed.
  • Detached DOM — nodes removed from the document but still referenced by JavaScript data structures or closures.
  • Unbounded caches — Maps/arrays storing request or object data forever.
  • Closures — a small callback can unintentionally retain a much larger lexical object graph.

Investigation usually involves reproducing growth, using browser memory tooling/heap snapshots, comparing retained objects over time, and following retaining paths to understand why an object is still reachable. Fix the ownership/cleanup rule rather than calling garbage collection manually.

3

Advanced JavaScript Interview Questions

Question 71AdvancedConcept

What are execution contexts and lexical environments in JavaScript, and how do they relate to scope?

Direct answer

An execution context represents the state needed to execute code, while lexical environments and environment records hold the bindings that lexical name resolution consults; scope lookup follows the chain of outer environments.

A useful senior-level model is to separate execution from name resolution. An execution context tracks the currently executing code and associated state. Lexical environments provide the structure used to resolve identifiers such as local variables, parameters, and imported bindings.

When code evaluates user.name, resolving user begins in the current environment record. If the binding is not there, resolution follows the outer environment references until it finds a binding or reaches the end of the chain. This is the mechanism behind lexical scope; it is more precise than saying JavaScript “searches parent functions.”

  • Function calls create new execution contexts and function-related environments.
  • Blocks can introduce lexical bindings for let, const, and class without creating a new function call.
  • Closures work because functions retain access to the lexical environment chain associated with where they were created.
Question 72AdvancedCode Output

How can lexical shadowing create a Temporal Dead Zone even when an outer variable has the same name?

Direct answer

A let, const, or class declaration creates a binding for its entire lexical scope, so that binding shadows an outer variable from the start of the scope even though it cannot be accessed before initialization.

lexical-shadowing-tdz.js
let value = "outer";
{
  console.log(value);
  let value = "inner";
}
Output
ReferenceError: Cannot access 'value' before initialization

Consider let value = "outer"; { console.log(value); let value = "inner"; }. The console.log does not fall back to the outer value. The block already has its own lexical binding named value, and that binding is still uninitialized at the log statement. The result is a ReferenceError.

This demonstrates why the TDZ is not simply “the lines between entering the block and the declaration.” The key is that the binding already exists for scope resolution, but accessing it before the declaration is evaluated is forbidden.

  • Shadowing chooses the nearest lexical binding.
  • The existence of an outer initialized variable does not bypass the inner TDZ.
  • The same reasoning is important when refactoring large blocks and adding a new let or const declaration with an existing name.
Question 73AdvancedConcept

How does JavaScript convert objects to primitive values, and what role does Symbol.toPrimitive play?

Direct answer

Object-to-primitive conversion first allows a Symbol.toPrimitive method to decide the result; otherwise JavaScript falls back to ordinary conversion using valueOf() and toString() in an order influenced by the requested conversion hint.

Operations such as string interpolation, numeric arithmetic, and loose equality can require an object to become a primitive. If the object defines obj[Symbol.toPrimitive], JavaScript calls it with a hint such as "number", "string", or "default". The method must return a primitive value.

For example, an object can implement [Symbol.toPrimitive](hint) { return hint === "number" ? 42 : "answer"; }. Then +obj can produce 42 while `${obj}` can produce "answer". This is intentional customization of coercion, not operator overloading in the general sense.

  • If Symbol.toPrimitive exists, it takes precedence over the ordinary fallback.
  • Ordinary conversion consults methods such as valueOf() and toString(); the exact preference depends on the hint.
  • Returning another object from Symbol.toPrimitive causes a TypeError because the conversion must produce a primitive.
Question 74AdvancedComparison

What is the difference between ===, Object.is(), and SameValueZero equality in JavaScript?

Direct answer

Strict equality treats +0 and -0 as equal and NaN as unequal to itself; Object.is() distinguishes signed zero and treats NaN as equal to itself; SameValueZero treats signed zero as equal and NaN as equal.

These algorithms differ only in a few edge cases, but those edge cases explain real API behavior. NaN === NaN is false, while Object.is(NaN, NaN) is true. Conversely, 0 === -0 is true, while Object.is(0, -0) is false.

SameValueZero combines the usually convenient choices: NaN compares equal to itself and signed zero compares equal. Collections and lookup operations such as Set, Map key matching, and Array.prototype.includes() use SameValueZero-style comparison.

  • Use === for ordinary strict comparisons.
  • Use Object.is() when NaN and signed zero distinctions matter.
  • Do not assume all collection membership checks use ===; APIs can specify a different equality algorithm.
Question 75AdvancedCode Output

How do sparse arrays and empty slots behave differently from array elements whose value is undefined?

Direct answer

An empty slot means the property for that index does not exist, while an element containing undefined is an existing property with the value undefined; many array methods observe that distinction.

sparse-vs-undefined.js
const a = [, 2];
const b = [undefined, 2];

console.log(0 in a, 0 in b);
console.log(a.map((x) => x * 2));
console.log(b.map((x) => x * 2));
Output
false true
[, 4]
[NaN, 4]

Compare const a = [ , 2 ]; const b = [undefined, 2];. Both arrays have length 2 and reading index 0 produces undefined, but 0 in a is false whereas 0 in b is true. The first array has a hole; the second has an actual element.

Many callback-based array methods such as map(), forEach(), and filter() skip missing indices rather than invoking the callback for a hole. Other mechanisms can materialize holes as undefined values, so changing iteration strategy can change observable behavior.

  • Array length is based on index range, not the number of existing properties.
  • Deleting an array element with delete arr[i] can create a hole without shrinking length.
  • When dense data is intended, prefer operations such as splice() or explicit filtering instead of leaving accidental holes.
Question 76AdvancedConcept

What ordering rules should you know when JavaScript enumerates an object’s own property keys?

Direct answer

For ordinary own-key ordering, array-index-like string keys are ordered numerically first, other string keys follow creation order, and Symbol keys follow creation order after the string keys; individual APIs can additionally filter which keys they return.

A common misconception is that object properties are either completely unordered or always simple insertion order. Modern JavaScript defines useful ordering rules for ordinary objects. Keys that are canonical array-index-like strings are handled before other strings, and those numeric-looking keys are ordered numerically rather than purely by insertion time.

For an object created as const x = { b: 1, 10: 2, 2: 3, a: 4 };, APIs that expose ordinary own string-key order will place "2" before "10", followed by non-index strings such as "b" and "a" in their creation order. Symbols form a later category in own-key order.

  • Object.keys() filters to enumerable own string keys.
  • Object.getOwnPropertyNames() includes non-enumerable own string keys.
  • Reflect.ownKeys() includes own string and Symbol keys.
Question 77AdvancedConcept

What are Symbols in JavaScript, and how do well-known Symbols customize language behavior?

Direct answer

A Symbol is a unique primitive often used as a collision-resistant property key, while well-known Symbols such as Symbol.iterator and Symbol.toPrimitive provide protocol hooks that let objects participate in built-in language operations.

Two calls to Symbol("id") create distinct values even though their descriptions match. That makes Symbols useful for keys that should not collide with ordinary string property names. They are still properties, not truly private storage.

Well-known Symbols are more powerful because JavaScript itself looks for them. Defining obj[Symbol.iterator] can make an object iterable by for...of and spread syntax. Defining Symbol.toPrimitive customizes object-to-primitive conversion. Other well-known hooks participate in matching, species construction, and related protocols.

  • Symbol-keyed properties are not returned by Object.keys().
  • Global-registry Symbols created by Symbol.for() are intentionally shared by key and are different from fresh Symbols.
  • Use private class fields when you need language-enforced class privacy; a Symbol key can still be discovered through reflection such as Reflect.ownKeys().
Question 78AdvancedConcept

How do Proxy and Reflect work together for JavaScript metaprogramming?

Direct answer

A Proxy intercepts specified internal object operations through traps, while Reflect provides function-style operations that closely correspond to those traps and is often the safest way to forward default behavior.

A proxy wraps a target and a handler. A get trap can observe or customize property reads, a set trap can intercept writes, and traps such as ownKeys, has, or construct intercept other language operations. This allows validation, virtualization, access tracking, reactive systems, and similar patterns.

Inside a trap, Reflect.get(target, key, receiver) is usually better forwarding code than simply evaluating target[key]. The Reflect operation mirrors the underlying semantics and preserves important arguments such as the receiver used by accessors.

  • Proxy behavior is operation-based: intercepting get does not automatically intercept every way a value can be discovered.
  • Proxies can add substantial conceptual complexity and may interfere with identity assumptions or debugging.
  • Use Reflect for forwarding rather than manually reimplementing default semantics unless the behavior truly needs to differ.
Question 79AdvancedScenario

What are Proxy invariants, and why can a Proxy trap throw even when the trap itself returns a valid-looking value?

Direct answer

Proxy traps must preserve fundamental guarantees of the target object; if a trap reports a result that contradicts a non-configurable property or another protected invariant, JavaScript throws a TypeError.

Imagine a target has a non-configurable, non-writable data property id whose value is 1. A proxy cannot honestly pretend that reading that property produces an unrelated value in cases where the specification requires the target’s fixed property state to be respected. The engine validates trap results against invariants.

Similar rules apply to operations such as reporting own keys, defining properties, changing prototypes, and preventing extensions. For example, a proxy cannot hide a non-configurable own property from ownKeys merely because the handler wants a cleaner public view.

  • Invariants prevent proxies from creating object models that violate core assumptions of the language.
  • The more constrained the target becomes—especially with non-configurable properties or a non-extensible state—the fewer lies a proxy is allowed to tell.
  • When writing transparent wrappers, forwarding with Reflect reduces accidental invariant violations.
Question 80AdvancedComparison

How do JavaScript private class fields differ from underscore conventions and WeakMap-based privacy?

Direct answer

Private class fields use language-enforced private names that can only be accessed where that private name is lexically declared; underscore properties are only conventions, while WeakMaps can provide externally associated private state with different composition trade-offs.

A field such as #balance is not a normal string property named "#balance". Access is syntactically restricted to code that has access to that private name, and arbitrary reflection does not expose it as a normal own key. This is stronger than naming a public property _balance.

Before private fields, a WeakMap keyed by instances was a common privacy pattern. It still has useful properties: state can live outside the object’s visible property set and the weak key does not by itself keep the instance alive. But it requires an external map and lookup rather than class-private syntax.

  • Use #private fields when the state naturally belongs to a class and should be enforced by the language.
  • Use WeakMap-associated metadata when privacy or metadata needs to be attached externally across objects without adding visible properties.
  • Private fields also perform a brand check: attempting access on an object that does not carry the declared private field throws rather than returning undefined.
Question 81AdvancedCode Output

What happens when a bound function is called with new, and how does new.target help distinguish constructor calls?

Direct answer

If a bound function is constructable and invoked with new, the bound thisArg is ignored because construction creates a new instance; bound arguments still participate, and new.target identifies whether code is executing through construction.

bound-constructor.js
function User(name) {
  this.name = name;
}

const Bound = User.bind({ name: "ignored" }, "Ada");
const u = new Bound();

console.log(u.name);
Output
Ada

Suppose function User(name) { this.name = name; } const Bound = User.bind({ name: "ignored" }, "Ada"); const u = new Bound();. The new instance receives name === "Ada". The object passed as the bound thisArg is not used as the constructed receiver.

This follows from the difference between a normal call and a construction operation. Binding this affects calls, but a constructor call must create and initialize a fresh receiver according to construction semantics. A bound function can still pre-bind leading arguments.

  • Inside an ordinary function, new.target is undefined during a normal call.
  • During construction, new.target identifies the constructor originally invoked with new and is useful in constructor-oriented abstractions.
  • Arrow functions are not constructable and do not become constructable merely by using bind().
Question 82AdvancedScenario

Why can instanceof fail for objects created in another browser realm, and why is Array.isArray() safer for arrays?

Direct answer

instanceof normally checks an object against a constructor’s prototype relationship, so an array created by another realm has that realm’s Array.prototype; Array.isArray() performs an array-specific brand check and works across realms.

An iframe has its own global object and its own intrinsic constructors. If an array comes from that iframe, then foreignArray instanceof window.Array can be false because the foreign array’s prototype chain contains the iframe’s Array.prototype, not the current window’s one.

By contrast, Array.isArray(foreignArray) is designed to answer whether the value is an actual Array regardless of which realm created it. This distinction matters in libraries that communicate across iframes or other realm boundaries.

  • Do not use instanceof Array as the strongest cross-realm array test.
  • The same general realm issue can affect assumptions involving other built-in constructors and prototype identity.
  • instanceof can also be customized through Symbol.hasInstance, so it is not universally equivalent to “was created by this exact constructor.”
Question 83AdvancedConcept

What are live bindings in ES modules, and how do they affect circular dependencies?

Direct answer

ES module imports are live read-only views of exported bindings rather than one-time copied values; this allows updates to be observed across modules, but cycles can expose bindings before initialization and therefore require careful dependency design.

If module A exports let count = 0 and later updates that binding, module B that imported count observes the current exported binding rather than a snapshot captured at import time. The importer cannot assign directly to that imported binding.

Circular module graphs are therefore not handled by simply executing one complete file and copying its exports into the next. Modules are linked so bindings can refer to one another, then evaluated according to dependency rules. In a cycle, attempting to use a lexical export before it has been initialized can fail in a TDZ-like way.

  • Live bindings help cycles exist without requiring every export to be copied eagerly.
  • Cycles are still risky when top-level initialization depends on another module in the same cycle already having executed.
  • A common design fix is to extract shared primitives or move work behind functions so evaluation-time dependencies are reduced.
Question 84AdvancedScenario

How can top-level await affect an ES module graph and application startup?

Direct answer

Top-level await makes module evaluation asynchronous; importers that depend on that module must wait for the relevant asynchronous dependency chain, which can delay execution and make cycles more difficult to reason about.

With top-level await, a module can pause its evaluation while an asynchronous operation completes. Modules that depend on it cannot simply continue as though the dependency had synchronously finished initializing. The async status can therefore propagate through dependent parts of the module graph.

This is useful for environment initialization or resources that truly must exist before dependent modules execute, but placing network requests or optional setup at top level can turn module loading into a hidden startup dependency. It can also make cyclic graphs substantially harder to reason about.

  • Prefer top-level await when asynchronous initialization is genuinely part of the module contract.
  • For optional or user-triggered work, an explicit async initialization function is often easier to control and test.
  • Dynamic import() can sometimes isolate asynchronous loading instead of forcing the initial dependency graph to wait.
Question 85AdvancedScenario

What is microtask starvation, and how can an endless Promise or queueMicrotask chain hurt browser responsiveness?

Direct answer

Browsers perform a microtask checkpoint after a task, and newly queued microtasks can keep extending that checkpoint; an unbounded microtask chain can therefore delay later tasks and rendering even though each individual callback is asynchronous.

Consider a function that calls queueMicrotask(loop) from inside every execution of loop. Each callback is short, but it schedules another microtask before the microtask checkpoint finishes. The browser can remain busy draining microtasks and may not reach the work needed for rendering or user interaction.

Promises can create the same pattern when each reaction immediately schedules another reaction. This is why “put it in a Promise so it does not block” is incorrect. Moving work to microtasks changes ordering; it does not guarantee that the main thread gets time to render.

  • Use microtasks for short ordering-sensitive follow-up work.
  • For visual work before a frame, requestAnimationFrame() is usually a better scheduling primitive.
  • For large CPU work, split the work across tasks or move it off the main thread when appropriate rather than recursively filling the microtask queue.
Question 86AdvancedCode Output

What is the output order when queueMicrotask(), Promise.then(), and setTimeout() schedule work inside one another?

Direct answer

Microtasks are drained in queue order, including microtasks appended while the checkpoint is running; timer callbacks are later tasks, and each such task can create another microtask checkpoint before the next task runs.

nested-microtasks.js
console.log("A");
queueMicrotask(() => {
  console.log("B");
  Promise.resolve().then(() => console.log("C"));
});
Promise.resolve().then(() => {
  console.log("D");
  queueMicrotask(() => console.log("E"));
});
setTimeout(() => console.log("F"), 0);
console.log("G");
Output
A
G
B
D
C
E
F

Consider console.log("A"); queueMicrotask(() => { console.log("B"); Promise.resolve().then(() => console.log("C")); }); Promise.resolve().then(() => { console.log("D"); queueMicrotask(() => console.log("E")); }); setTimeout(() => console.log("F"), 0); console.log("G");.

The synchronous output is A, G. The first queued microtasks are the callback printing B and the Promise reaction printing D, so they run as B, D. While those run they append C and E to the same microtask queue, which are then drained as C, E. Only after that checkpoint can the timer task print F. Final order: A, G, B, D, C, E, F.

  • Both Promise reactions and queueMicrotask() schedule microtask-style work in the browser event-loop model.
  • Microtasks scheduled by earlier microtasks are appended and still run before moving on to the later timer task.
  • Do not infer ordering merely from which API call appears “more asynchronous.” Track the actual queue each callback enters.
Question 87AdvancedCode Output

Does await always yield, even when the awaited value is not a pending Promise?

Direct answer

Yes: evaluating await suspends the async function and its continuation resumes asynchronously through Promise-related job scheduling, even when the value is a plain value or an already-fulfilled Promise.

await-yields.js
async function f() {
  console.log("A");
  await 123;
  console.log("B");
}

console.log("C");
f();
console.log("D");
Output
C
A
D
B

Consider async function f(){ console.log("A"); await 123; console.log("B"); } console.log("C"); f(); console.log("D");. The output starts C, A, D, and B appears later when the async function continuation runs.

A plain value supplied to await is treated through the Promise-resolution machinery. The async function does not simply continue synchronously because the value is already available. This property gives await a consistent suspension boundary.

  • Do not use await casually in hot loops when no asynchronous dependency exists; every suspension changes scheduling and can add overhead.
  • An already-fulfilled Promise still resumes through asynchronous job processing rather than inline continuation.
  • This is why inserting or removing an await can change observable ordering with surrounding Promise callbacks.
Question 88AdvancedScenario

How would you prevent an older asynchronous request from overwriting a newer result in a JavaScript UI?

Direct answer

Treat the problem as ordering, not just cancellation: cancel obsolete work when possible and also guard result application with a request identity, sequence number, or current-state check so stale completions cannot commit.

Suppose search request A starts for "rea", then request B starts for "react". B may finish first. If A later resolves and blindly writes to the UI, the interface regresses to stale data even though every Promise completed successfully.

A robust pattern increments a request version and captures the version for each request. Before committing its result, the handler verifies that its version is still the current one. If the underlying API supports cancellation, an AbortController can additionally stop obsolete network work, but correctness should not rely on cancellation always winning the race.

  • Cancellation saves resources; a commit guard protects correctness.
  • State libraries often encode the same concept using request IDs, latest-only operators, or reducer checks.
  • The same race appears in autosuggest, route loading, validation, image processing, and any UI where input changes faster than asynchronous work completes.
Question 89AdvancedPractical

How would you limit concurrency when processing thousands of asynchronous operations instead of calling Promise.all() on all of them?

Direct answer

Use a bounded worker pool, semaphore, queue, or chunking strategy so only a controlled number of operations are in flight at once; this preserves concurrency without overwhelming remote services, memory, sockets, or the browser.

Calling Promise.all(items.map(doWork)) starts every mapped operation immediately if doWork initiates work synchronously. For a few items this is ideal. For tens of thousands of network or resource-heavy operations it can create a burst far beyond what the service or client should handle.

A bounded approach might start, for example, eight worker loops. Each worker takes the next item, awaits it, records the result, then takes another. At most eight operations are active. Another common abstraction is a semaphore that requires acquiring a permit before starting work and releases it in finally.

  • Choose the limit based on the bottleneck: API rate limits, browser connection behavior, memory, CPU, or downstream capacity.
  • Preserve result ordering separately if callers require outputs aligned with the original input order.
  • Decide failure semantics explicitly: fail fast, collect all errors, retry selected failures, or continue best-effort.
Question 90AdvancedComparison

When should you use for await...of instead of Promise.all()?

Direct answer

Use Promise.all() when you already have a finite set of independent operations whose results can be awaited together; use for await...of when values arrive over time through an async iterable, when you need incremental processing, or when backpressure-like sequential consumption matters.

If you have ten independent fetches that should start together and you need all results, Promise.all() expresses that intent well. By contrast, an async iterable may produce values only as data becomes available, perhaps from pages, streams, queues, or an async generator. for await...of consumes those values incrementally.

Using for await...of over a source does not automatically mean the underlying work is sequential; that depends on how the async iterator produces values. But the loop itself awaits each iteration result before advancing, giving the producer and consumer a natural coordination point.

  • Finite independent batch → often Promise.all().
  • Potentially long-lived or incremental async sequence → often for await...of.
  • If you need bounded parallelism over a stream, combine async iteration with an explicit concurrency-control strategy rather than choosing between only “one at a time” and “everything at once.”
Question 91AdvancedConcept

How do async generators support incremental asynchronous data production, and how should cleanup be handled?

Direct answer

An async generator combines async function suspension with generator yielding, producing an async iterator whose next() calls return Promises; consumers can process values incrementally, and producer cleanup should live in try/finally so iterator termination releases resources.

An async generator declared with async function* can await asynchronous work and then yield values one at a time. Calling next() returns a Promise for an iterator result, which is why for await...of is the natural consumer syntax.

This structure is useful for paginated APIs, event or message sources, database-like cursors exposed to JavaScript, and transformations where producing the entire result array first would waste memory or delay first output. The consumer asks for successive results rather than receiving one giant Promise for all data.

  • Place resource release in finally around the producer loop.
  • When a consumer exits a for await...of loop early, iterator closing gives the iterator a chance to terminate rather than silently leaving the producer active.
  • For real streams, also understand the stream API’s own cancellation and backpressure semantics; async iteration is an interface, not a universal replacement for stream-specific controls.
Question 92AdvancedComparison

When should you use a Web Worker instead of ordinary async JavaScript on the main thread?

Direct answer

Use async APIs to avoid waiting synchronously on I/O, but use a Worker when substantial JavaScript computation itself would occupy the main thread; a worker executes in a separate worker agent/global environment and communicates by messages rather than sharing the DOM.

Turning a CPU-heavy loop into async function calculate(){ ... } does not move the loop off the main thread. If the loop runs for 200 ms without yielding, the UI can still freeze for that period. async changes Promise-based control flow; it does not create another execution thread.

A dedicated Worker runs code in a separate worker environment. The page communicates with it using postMessage() and message events. This makes workers appropriate for CPU-heavy parsing, image/data transforms, compression, large calculations, and similar tasks when main-thread responsiveness matters.

  • Workers do not have normal direct access to the page DOM.
  • Communication has serialization or transfer costs, so tiny work can be slower when moved to a worker.
  • Measure the real bottleneck before introducing worker architecture; network waiting is usually not fixed by adding a worker.
Question 93AdvancedComparison

What is the difference between cloning and transferring data when communicating with a Web Worker?

Direct answer

Structured cloning creates an independent serialized/deserialized value in the destination realm, while transferring moves ownership of transferable resources such as an ArrayBuffer’s backing data so the original sender can no longer use that transferred buffer.

Calling worker.postMessage(data) normally uses the platform’s structured serialization machinery for supported values. For large binary payloads this can imply copying or reconstruction work. With a transfer list, code can instead transfer certain resources.

For example, worker.postMessage({ buffer }, [buffer]) transfers the ArrayBuffer rather than keeping the same usable backing store on both sides. The sender’s transferred buffer becomes detached, while the receiving side gets ownership of the transferred data.

  • Transfer when ownership can safely move and avoiding a large copy matters.
  • Clone when both sides need independent usable values.
  • A transfer is not a magical shared-memory operation. If actual shared memory is needed, that is a different model involving SharedArrayBuffer and synchronization.
Question 94AdvancedConcept

Why do SharedArrayBuffer and Atomics require a different mental model from ordinary JavaScript object access?

Direct answer

SharedArrayBuffer can expose the same underlying memory to multiple agents, so ordinary assumptions based on isolated object state are insufficient; Atomics provides synchronization and atomic memory operations needed for well-defined coordination on supported typed-array views.

Ordinary ArrayBuffer data is commonly cloned or transferred between agents. SharedArrayBuffer instead enables multiple agents to access shared backing memory. Once true shared memory exists, independent reads and writes can race.

The Atomics APIs provide operations such as atomic loads, stores, arithmetic updates, compare-and-exchange, and waiting/notification mechanisms on compatible typed-array views. These primitives let code coordinate shared state without pretending that unsynchronized reads and writes form a safe protocol.

  • Use message passing by default when it keeps the design simpler.
  • Shared memory is appropriate for specialized high-performance or low-level coordination where its complexity is justified.
  • Correctness requires reasoning about synchronization, not merely whether each individual line of JavaScript is “single-threaded.”
Question 95AdvancedConcept

Why should WeakRef and FinalizationRegistry not be used for deterministic resource cleanup?

Direct answer

Garbage collection timing is intentionally non-deterministic and the specification does not guarantee that an unreachable target will be collected promptly—or at all—so WeakRef and finalization callbacks cannot provide reliable timing for releasing critical resources.

A WeakRef can allow code to observe a target without creating an ordinary strong reference that necessarily keeps it live. FinalizationRegistry can register cleanup-related callbacks associated with targets. These features are deliberately constrained because garbage collection is an implementation concern with substantial optimization freedom.

The engine may keep an otherwise unreachable object for an arbitrary period, and cleanup callbacks are not guaranteed to run at a predictable moment. Therefore closing a file-like resource, releasing a lock, committing data, or performing any correctness-critical action should use explicit lifecycle code rather than waiting for GC.

  • Good use cases are cache-like or auxiliary behavior where cleanup is opportunistic.
  • Do not write logic whose correctness depends on a finalizer running before process/page termination.
  • Avoid repeatedly calling deref() and assuming a weak target’s lifetime is stable across asynchronous boundaries.
Question 96AdvancedPractical

How would you diagnose whether a slow interaction is caused by JavaScript execution, layout thrashing, or rendering work?

Direct answer

Profile the interaction in browser performance tooling, identify long main-thread tasks, inspect the breakdown of scripting/style/layout/paint work, then correlate expensive call stacks and forced layout events with the application code that triggered them.

Start with measurement rather than guessing from source code. Record the exact slow interaction. If a long task is dominated by JavaScript, inspect the call tree or bottom-up profile to find functions consuming CPU. If repeated layout calculations appear between script operations, look for code that alternates layout-dependent reads with DOM/style writes.

A common layout-thrashing pattern is element.style.width = ...; const x = element.offsetWidth; element.style.height = ...; const y = element.offsetHeight; repeated across many elements. Reads such as geometry queries can force the browser to bring layout state up to date after writes, turning a batchable rendering pipeline into repeated synchronous work.

  • Batch DOM reads together and DOM writes together when possible.
  • Move CPU-heavy pure computation away from the main thread if profiling proves it is the dominant problem.
  • Do not optimize based only on function call counts; rendering cost and allocation/GC behavior can dominate even when individual JavaScript functions look small.
Question 97AdvancedComparison

When should browser UI code use requestAnimationFrame(), microtasks, or timers?

Direct answer

Use microtasks for short ordering-sensitive follow-up before returning control to later tasks, requestAnimationFrame() for visual updates coordinated with an upcoming rendering opportunity, and timers for task scheduling after at least a delay—not for precise frame synchronization.

A microtask scheduled with queueMicrotask() runs during a microtask checkpoint and can occur before the browser gets another rendering opportunity. That makes microtasks excellent for normalizing ordering, but a poor mechanism for repeatedly performing heavy visual work.

A requestAnimationFrame() callback is specifically associated with browser rendering opportunities and is therefore the natural place to calculate or apply animation-frame updates. A setTimeout() callback is a later task subject to delay clamping and scheduling conditions; a delay of zero never means “immediately” or “exactly before the next paint.”

  • Ordering cleanup or deferred notification within the same turn → microtask.
  • Per-frame visual update → requestAnimationFrame().
  • Run later after a delay or yield into a later task → timer/task-oriented scheduling, while accepting timing is not exact.
Question 98AdvancedScenario

What is prototype pollution in JavaScript, and how can applications reduce the risk?

Direct answer

Prototype pollution occurs when untrusted property paths or merge logic modify a shared prototype such as Object.prototype, causing attacker-controlled properties to appear on unrelated objects; prevent it with safe key handling, schema validation, safer data structures, and null-prototype dictionaries where appropriate.

A dangerous generic setter might accept a path like "__proto__.isAdmin" or traverse through constructor.prototype and assign attacker-controlled data. If that reaches a shared prototype, later code may observe obj.isAdmin on objects that never had their own property. The exact exploitability depends on how polluted values are later consumed.

Defenses start by not treating arbitrary attacker-controlled strings as unrestricted object paths. Validate expected schemas, reject dangerous keys where dynamic assignment is unavoidable, and prefer Map for dictionary-like data when prototype inheritance is not part of the model. Object.create(null) can create a dictionary object without Object.prototype in its chain.

  • Use own-property checks when the distinction between own and inherited data matters.
  • Keep dependency versions current because unsafe merge/path utilities have historically been a common vector.
  • Freezing selected prototypes can be defense in depth in controlled environments, but it can break code that intentionally modifies them and is not a substitute for safe input handling.
Question 99AdvancedComparison

What is the difference between a polyfill and transpilation, and why can some JavaScript features not be perfectly polyfilled?

Direct answer

Transpilation rewrites source syntax into code a target environment can parse and run, while a polyfill adds missing runtime APIs or behavior; features that require new syntax, engine internals, or unexposable semantics cannot always be faithfully implemented by ordinary library code.

A transpiler can transform syntax such as newer class or optional-chaining constructs into older syntax where a semantics-preserving transform is possible. A polyfill instead might define a missing method such as Array.prototype.someFeature using capabilities the older runtime already exposes. Real compatibility stacks often need both.

Not every language capability is just a missing function. New syntax must be understood before code can run at all, which is why syntax requires parsing/transformation rather than a library loaded afterward. Some semantics also depend on engine-level behavior or internal slots that userland objects cannot perfectly reproduce.

  • Transpilation solves source compatibility.
  • Polyfills solve selected runtime capability gaps.
  • Target only the environments you support; blindly shipping every transform and polyfill can increase bundle size, startup work, and debugging complexity.
Question 100AdvancedConcept

Why do ES modules make tree shaking easier, and why can side effects still prevent dead-code elimination?

Direct answer

ES import/export structure is statically analyzable, so build tools can reason about which exports are referenced; however, unused-looking modules or statements may still have observable side effects, so safely removing them requires side-effect analysis and build configuration rather than merely seeing an unused name.

Static import and export declarations give bundlers a dependency graph they can analyze without executing arbitrary module-loading code. This makes it possible to determine that certain exported bindings are never referenced by the reachable application graph.

But an unused export does not mean every statement that produced it can disappear. Importing a module can execute top-level code: registering something globally, modifying a prototype, initializing telemetry, importing CSS through a toolchain, or performing other observable work. Removing that module could change behavior.

  • Prefer modules with explicit exports and limited top-level side effects when you want optimization to be predictable.
  • Package metadata that declares side-effect characteristics is a build-tool contract and must be accurate; marking effectful modules as side-effect-free can break applications.
  • Tree shaking is a build-time optimization enabled by analyzable module structure, not a runtime feature guaranteed by the JavaScript engine.