Experienced interview questions
A closure is a function that remembers and can still access the variables from the scope in which it was created, even after that outer function has finished running. Closures are what make patterns like private variables and function factories possible in JavaScript, since the inner function keeps a live reference to its outer variables rather than a copy of their values at creation time.
The event loop is the mechanism that lets JavaScript, despite running on a single thread, handle asynchronous work without blocking. Synchronous code runs first on the call stack; once the stack is empty, the event loop pulls queued callbacks, such as a finished timer or a completed network request, and pushes them onto the stack to run, which is why asynchronous code always runs after the current synchronous code finishes, no matter how short its delay.
The call stack tracks the functions currently being executed, following a last-in-first-out order as functions call other functions. The task queue holds callbacks that are ready to run once the call stack is completely empty, such as a setTimeout callback or a resolved promise handler. The event loop constantly checks whether the stack is empty and, if so, moves the next item from the queue onto it.
Microtasks, which include promise callbacks and queueMicrotask, are given higher priority and always run to completion before the event loop moves on to the next macrotask. Macrotasks include things like setTimeout callbacks, user interface events and I/O. In practice this means a resolved promise's handler runs before a setTimeout scheduled with a zero-millisecond delay, even though both appear to be waiting for the same moment.
A promise is an object representing a value that is not available yet but will be at some point, either successfully or with an error. It can be in one of three states: pending, fulfilled or rejected. Once a promise settles, its result is delivered to functions registered with .then for success or .catch for failure, and this settled state and value are cached, so attaching a handler later still receives the correct result.
Both accomplish the same underlying behavior, since async/await is essentially a more readable way to work with promises. Chaining .then calls can become difficult to read once there are several sequential steps or nested conditions, sometimes called callback pyramid problems. async/await lets that same logic be written in a linear, top-to-bottom style that closely resembles synchronous code, with errors handled using an ordinary try/catch block instead of a .catch call.
Every JavaScript object has an internal link to another object, called its prototype, from which it can inherit properties and methods. When a property is accessed on an object and not found directly on it, JavaScript automatically looks up the prototype chain, checking the prototype, then that prototype's own prototype, and so on, until the property is found or the chain ends at null.
Classical inheritance, found in languages like Java, defines fixed classes at compile time, and objects are instances of those classes. Prototypal inheritance, JavaScript's native model, has objects inherit directly from other live objects through the prototype chain, with no separate blueprint required. JavaScript's class syntax is popular sugar written on top of this same prototypal system rather than a truly separate inheritance model.
this refers to the object a function is currently executing as a method of, and its value is determined by how the function is called, not where it is defined. Calling obj.method() sets this to obj, calling a function on its own sets this to undefined in strict mode or the global object otherwise, and arrow functions ignore all of this, instead inheriting this from their surrounding lexical scope.
call and apply both invoke a function immediately with a specified this value, differing only in how additional arguments are passed: call takes them individually while apply takes them as a single array. bind does not invoke the function immediately; instead it returns a brand new function permanently bound to the given this value, useful when a function needs to be passed around and called later with the correct context.
The temporal dead zone is the period between when a let or const variable enters scope, due to hoisting, and when its declaration line actually executes. Trying to access the variable during this window throws a ReferenceError rather than returning undefined, which is a deliberate safety improvement over how var silently allows access before its declaration.
Currying is transforming a function that takes multiple arguments into a sequence of functions that each take a single argument, one at a time, with each call returning a new function until all arguments have been supplied. It is often used to create specialized, reusable versions of a more general function by pre-filling some of its arguments in advance.
A higher-order function is a function that either accepts another function as an argument, returns a function as its result, or both. Array methods like map, filter and reduce are common examples, since each takes a callback function as an argument, and this pattern is central to functional programming style in JavaScript.
Debouncing delays running a function until a certain amount of time has passed without the triggering event happening again, resetting the timer every time the event fires in the meantime. It is commonly applied to a search input, so a network request only fires once the user has actually paused typing, rather than on every single keystroke.
Throttling ensures a function runs at most once within a given time interval, no matter how many times the triggering event fires during that interval. It is commonly used on scroll or resize event handlers, which can fire dozens of times per second, to limit expensive work like layout recalculations to a manageable, steady rate.
Common causes include forgotten timers or intervals that are never cleared, event listeners attached to elements that are removed from the page without being detached first, and references to objects held inside a global variable or a growing cache that is never cleaned up. Since the garbage collector only frees memory for objects with no remaining references, anything still referenced, even accidentally, stays in memory indefinitely.
JavaScript uses automatic garbage collection, most commonly based on an algorithm called mark-and-sweep. The collector periodically starts from a set of root references, such as global variables and currently executing functions, and marks every object reachable from them; anything left unmarked, meaning nothing in the program can reach it anymore, is considered garbage and its memory is reclaimed.
CommonJS, the module system originally used by Node.js, uses require to import and module.exports to export, and it loads modules synchronously. ES modules, the standard built into the language itself, use import and export keywords, support static analysis so tools can determine dependencies without running the code, and load asynchronously, which is better suited to browsers. Modern Node.js supports both, though ES modules are increasingly the default going forward.
The module pattern uses an immediately invoked function expression to create a private scope, exposing only a chosen set of functions or values while keeping everything else hidden from outside code. It was a common way to achieve encapsulation and avoid polluting the global scope before native ES modules existed, and the same underlying idea still shows up inside modern module files.
The singleton pattern ensures a particular object is only ever created once, with every part of the application sharing that same single instance rather than creating separate copies. In JavaScript this is often achieved simply by exporting an already-created object from a module, since a module's exports are cached and reused every time it is imported elsewhere in the program.
A Web Worker runs JavaScript code on a separate background thread, away from the main thread that handles the user interface. This allows heavy computational work, like processing a large dataset or performing complex calculations, to run without freezing the page, since workers communicate with the main thread through messages rather than sharing memory directly.
Synchronous code runs one line at a time in order, with each line waiting for the previous one to finish before starting, which blocks everything else while a slow operation completes. Asynchronous code allows a slow operation, like a network request, to run in the background while the rest of the program continues, with the result delivered later through a callback, a promise, or await.
Cross-site scripting, or XSS, is a vulnerability where an attacker manages to inject malicious JavaScript into a page that then runs in another user's browser, often by exploiting user input that gets inserted into the page without proper escaping. Front-end code that sets innerHTML directly from unsanitized user input is a classic way this happens, which is why frameworks and safe DOM APIs generally escape content by default.
Cross-site request forgery, or CSRF, tricks a logged-in user's browser into unknowingly sending a request to a different site where they are authenticated, potentially performing an action like changing account settings without their consent. It is primarily defended against on the server with tokens that verify a request genuinely originated from the real application, though front-end code plays a role in correctly including those tokens with requests.
Event delegation attaches a single event listener to a shared parent element rather than attaching a separate listener to every individual child element, relying on the fact that events bubble upward through the DOM. Inside the one handler, event.target identifies which specific child was actually interacted with. This is more efficient for long or dynamically changing lists, since new children are automatically covered without needing their own listener attached.
Shallow equality compares two values directly, meaning two objects are only considered equal if they are literally the exact same reference in memory, even if their contents look identical. Deep equality instead compares the actual contents of two objects or arrays recursively, considering them equal if every corresponding value matches, regardless of whether they are the same object in memory.
Memoization is an optimization technique where the result of an expensive function call is cached against its input, so that calling the function again with the same input returns the stored result immediately instead of recalculating it. It trades memory for speed and is especially effective for pure functions, whose output only ever depends on their input and never changes for the same arguments.
A regular function gets its own this value, determined dynamically by how it is called, which can change depending on the calling context. An arrow function has no this of its own at all; it simply uses whatever this value existed in the surrounding scope where it was defined. This makes arrow functions predictable inside callbacks, such as inside a class method, where a regular function's this would otherwise unexpectedly change.
A generator function, defined with an asterisk after the function keyword, can pause its own execution partway through and later resume exactly where it left off, rather than running straight through to completion like a normal function. Calling a generator function does not run its code immediately; instead it returns an iterator that produces one value each time its next method is called.
yield is used inside a generator function to pause execution at that point and return a value to whoever is calling the generator's next method. Execution stays paused there until next is called again, at which point the generator resumes right after the yield statement, allowing generators to produce a sequence of values over time rather than all at once.
A Proxy wraps a target object and intercepts fundamental operations on it, such as reading a property, setting a property, or checking whether a property exists, allowing custom logic to run for those operations. It is used for things like validation, logging every access to an object, or creating reactive data structures that automatically detect when their properties change, which is how some front-end frameworks implement reactivity under the hood.
const prevents a variable from being reassigned to point at a different value, but it says nothing about whether the object that variable points to can be modified internally, so its properties can still be changed freely. Object.freeze operates on the object itself, preventing any of its existing properties from being changed, added or removed, regardless of which variable or how many variables reference that object.
Common approaches include minimizing and batching DOM updates rather than changing the page repeatedly in a loop, using debouncing or throttling on frequent events like scroll and resize, lazy-loading images and other off-screen content, reducing the size and number of JavaScript bundles sent to the browser, and using browser developer tools to profile exactly which function or render step is actually consuming the most time before optimizing blindly.
Tree shaking is a build-time optimization where a bundler analyzes which parts of a module are actually imported and used, then eliminates the unused exports entirely from the final bundle. It relies on the static, analyzable structure of ES module imports and exports, which is one practical reason ES modules are preferred over CommonJS for library code intended to be bundled efficiently.
Imperative code describes the exact step-by-step instructions for how to accomplish something, such as manually creating an element, setting its attributes, and appending it to the page one operation at a time. Declarative code instead describes what the final result should look like, leaving the how up to something else. React embraces this by letting developers describe the desired UI for a given state, while React itself figures out the actual DOM operations needed to get there.
A pure function always returns the same output for the same input and produces no observable side effects, such as modifying an external variable, changing its arguments, or making a network request. Pure functions are easier to test, reason about and reuse safely, since calling one never depends on or affects anything outside of the values explicitly passed into it.
Immutability means data is never changed after it is created; instead, any update produces a brand new copy with the change applied, leaving the original untouched. Many front-end frameworks rely on this to efficiently detect changes, since comparing whether two references are identical is far cheaper than deeply comparing every field, and it also prevents an entire category of bugs caused by unexpected shared mutation.
A Map holds strong references to its keys, meaning an object used as a key will not be garbage collected as long as the Map still exists, even if nothing else references it. A WeakMap holds its keys weakly, allowing them to be garbage collected once nothing else references them, which makes WeakMap well suited for attaching extra data to an object without accidentally preventing that object from ever being cleaned up.
A service worker is a script that runs in the background, separate from the page itself, capable of intercepting network requests, caching assets for offline use, and enabling features like push notifications. It forms the technical foundation of progressive web apps, letting a website continue to function, at least partially, even without a network connection.
With client-side rendering, the browser receives a mostly empty HTML page and JavaScript builds the actual content in the browser after loading, which can delay the first meaningful paint but keeps subsequent navigation fast. With server-side rendering, the server generates the full HTML for a page before sending it, so content appears immediately and search engines can read it more easily, often at the cost of a heavier server workload per request.