1. What is the difference between var, let and const?
var is function-scoped and can be redeclared and updated, while let and const are block-scoped. let allows reassignment but not redeclaration in the same scope, and const forbids reassignment of the binding. A const object can still be mutated, because only the binding is fixed, not the value. All three are hoisted, but let and const stay in the temporal dead zone until their declaration runs, so using them early throws a ReferenceError.
if (true) {
var a = 1;
let b = 2;
const c = 3;
}
console.log(a);
console.log(b);
2. What is hoisting in JavaScript?
Hoisting is JavaScript's behavior of moving declarations to the top of their scope during the compile phase, before code runs. Function declarations are hoisted with their body, so you can call them before they appear. var variables are hoisted and initialized to undefined, while let, const and class are hoisted but not initialized, which creates the temporal dead zone. Function expressions and arrow functions are only as hoisted as the variable that holds them.
console.log(x);
var x = 5;
sayHi();
function sayHi() { console.log("hi"); }
3. What is a closure in JavaScript?
A closure is a function that remembers the variables of the scope where it was created, even after that outer function has finished running. Every function in JavaScript forms a closure over its lexical environment. Closures are used for data privacy, factory functions, memoization, and callbacks that need to keep state. A common pitfall is creating closures inside loops with var, where all of them share one variable.
function counter() {
let count = 0;
return () => ++count;
}
const next = counter();
next();
next();
4. What is the difference between == and ===?
== is the loose equality operator that converts operand types before comparing, while === is strict equality that compares both type and value with no conversion. For example, 0 == "0" is true but 0 === "0" is false. The coercion rules of == are hard to remember and cause bugs, so most style guides require ===. The main accepted use of == is value == null, which matches both null and undefined. Note that NaN is not equal to anything, even itself; use Number.isNaN() or Object.is().
5. How does the `this` keyword work in JavaScript?
The value of this is decided by how a function is called, not where it is defined. In a method call it is the object before the dot, in a plain function call it is undefined in strict mode (or the global object in sloppy mode), and with new it is the newly created object. call, apply and bind set it explicitly. Arrow functions have no own this and use the one from the surrounding scope. In event handlers using addEventListener with a regular function, this is the element.
const user = {
name: "Ana",
hi() { return this.name; }
};
user.hi();
const f = user.hi;
f();
6. What are the data types in JavaScript?
JavaScript has seven primitive types, string, number, bigint, boolean, undefined, null and symbol, plus the object type, which includes arrays, functions, dates and maps. Primitives are immutable and compared by value, while objects are compared by reference. The typeof operator returns the type name, with two well-known quirks: typeof null is "object" (a historical bug) and typeof function(){} is "function". Use Array.isArray() to check for arrays.
7. What is the difference between null and undefined?
undefined means a variable has been declared but not assigned a value, while null is an explicit value that represents "no value" or "empty". JavaScript itself produces undefined for missing properties, missing arguments and functions without a return. Developers assign null on purpose. null == undefined is true but null === undefined is false, and typeof null is "object" while typeof undefined is "undefined". The nullish operators ?? and ?. treat both the same way.
8. What are arrow functions and how do they differ from regular functions?
Arrow functions are a shorter function syntax introduced in ES6 that do not have their own this, arguments, super or new.target. They inherit this from the enclosing scope, which makes them ideal for callbacks. They cannot be used as constructors with new, and they have no prototype property. They are also a poor choice for object methods that rely on this. A single-expression body returns its value implicitly.
const add = (a, b) => a + b;
const obj = {
n: 1,
later() { setTimeout(() => console.log(this.n), 0); }
};
9. What is the event loop in JavaScript?
The event loop is the mechanism that lets single-threaded JavaScript handle asynchronous work by moving finished tasks onto the call stack when it is empty. Synchronous code runs first on the call stack. Then the loop drains the microtask queue (promise callbacks, queueMicrotask) completely, and only after that takes one task from the macrotask queue (setTimeout, I/O, UI events). This is why a resolved promise callback always runs before a setTimeout of 0 ms.
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
10. What is a Promise and what states can it have?
A Promise is an object that represents the eventual result of an asynchronous operation, and it is always in one of three states: pending, fulfilled or rejected. Once settled, a promise can never change state again. You consume it with .then() for success, .catch() for errors and .finally() for cleanup, and these calls return new promises so they can be chained. Callbacks passed to .then() always run asynchronously as microtasks.
fetch("/api/user")
.then((res) => res.json())
.then((user) => console.log(user))
.catch((err) => console.error(err));
11. How does async/await work, and how do you handle errors with it?
async/await is syntax built on promises that lets you write asynchronous code that reads like synchronous code. An async function always returns a promise, and await pauses that function until the awaited promise settles, without blocking the main thread. Errors are handled with a normal try/catch block, since a rejected promise makes await throw. To run independent tasks in parallel, start them first and await them together with Promise.all.
async function load() {
try {
const res = await fetch("/api/user");
return await res.json();
} catch (e) {
console.error(e);
}
}
12. What is the difference between Promise.all, allSettled, race and any?
All four combine several promises but differ in when and how they settle. Promise.all resolves with an array of results once every promise fulfills and rejects immediately on the first rejection. Promise.allSettled always waits for all of them and returns each outcome as a status object. Promise.race settles as soon as the first promise settles, whether it fulfills or rejects. Promise.any resolves with the first fulfilled promise and rejects with an AggregateError only if all of them reject.
13. What is the difference between map, filter and reduce?
map transforms every element and returns a new array of the same length, filter returns a new array containing only the elements that pass a test, and reduce folds the array into a single value using an accumulator. None of them mutate the original array. Use forEach when you only need side effects, because it returns nothing. Always pass an initial value to reduce, otherwise an empty array throws a TypeError.
const nums = [1, 2, 3, 4];
nums.map((n) => n * 2);
nums.filter((n) => n % 2 === 0);
nums.reduce((sum, n) => sum + n, 0);
14. What are call, apply and bind in JavaScript?
All three let you set the value of this for a function explicitly. call invokes the function immediately with this and arguments passed one by one, apply does the same but takes the arguments as an array, and bind does not invoke the function but returns a new function with this permanently fixed. bind can also pre-fill leading arguments, which is called partial application.
function greet(greeting) {
return greeting + ", " + this.name;
}
const user = { name: "Sam" };
greet.call(user, "Hi");
greet.apply(user, ["Hello"]);
const bound = greet.bind(user);
15. What is the spread operator and what is the rest parameter?
Both use the ... syntax but do opposite things. The spread operator expands an iterable or object into individual elements, for example to copy or merge arrays and objects or to pass an array as function arguments. The rest parameter collects the remaining arguments or properties into a single array or object. Spread makes only a shallow copy, so nested objects are still shared by reference.
const a = [1, 2];
const b = [...a, 3];
function sum(...nums) {
return nums.reduce((x, y) => x + y, 0);
}
16. What is destructuring in JavaScript?
Destructuring is a syntax that unpacks values from arrays or properties from objects into separate variables in a single statement. Array destructuring is based on position, while object destructuring is based on property names. You can set default values, rename variables, skip items, and use nested patterns. It is commonly used for function parameters, swapping variables, and extracting data from API responses.
const { name, age = 18, city: town } = user;
const [first, , third] = [1, 2, 3];
let x = 1, y = 2;
[x, y] = [y, x];
17. What are truthy and falsy values in JavaScript?
A falsy value is one that becomes false when converted to a boolean, and every other value is truthy. There are exactly eight falsy values: false, 0, -0, 0n, "", null, undefined and NaN. Surprisingly, empty arrays [], empty objects {} and the string "0" are all truthy. The || operator returns the first truthy operand, while ?? only falls back on null or undefined, so it keeps valid values like 0 and "".
18. How does prototypal inheritance work in JavaScript?
Every JavaScript object has an internal link to another object called its prototype, and when you access a property that is not found on the object, the engine follows this chain upward until it finds the property or reaches null. This is how objects share methods without copying them. You can set the prototype with Object.create(), constructor functions and their prototype property, or class ... extends. Use Object.getPrototypeOf() to inspect the chain rather than the older __proto__.
const animal = { speak() { return "..."; } };
const dog = Object.create(animal);
dog.speak();
19. What are ES6 classes, and are they different from prototypes?
ES6 classes are syntactic sugar over the existing prototype-based inheritance, not a new object model. A class defines a constructor and methods that are placed on the prototype, and extends sets up the prototype chain, with super calling the parent. Unlike plain constructor functions, classes are not usable before declaration (they are in the temporal dead zone), always run in strict mode, and throw an error if called without new. Modern classes also support #private fields and static members.
class Animal {
#name;
constructor(name) { this.#name = name; }
get name() { return this.#name; }
}
class Dog extends Animal {}
20. What is the difference between a shallow copy and a deep copy?
A shallow copy duplicates only the top-level properties, so nested objects are still shared by reference with the original, while a deep copy duplicates everything recursively. Spread ({...obj}), Object.assign and Array.from make shallow copies. For deep copies, use the built-in structuredClone(), which handles dates, maps, sets and circular references. JSON.parse(JSON.stringify(obj)) is a common shortcut but it drops functions, undefined and turns dates into strings.
const a = { x: { y: 1 } };
const shallow = { ...a };
const deep = structuredClone(a);
shallow.x.y = 2;
21. What is the difference between a function declaration and a function expression?
A function declaration uses the function name() {} statement and is fully hoisted, so you can call it before its position in the code. A function expression assigns a function to a variable, such as const f = function() {}, and follows the hoisting rules of that variable, so calling it earlier throws an error with let or const. Expressions can be anonymous and are also used for callbacks and immediately invoked function expressions (IIFE).
hello();
function hello() {}
bye();
const bye = function () {};
22. What are higher-order functions and callbacks?
A higher-order function is a function that takes another function as an argument, returns a function, or both. A callback is the function that is passed in to be called later. This works because functions are first-class values in JavaScript. Built-ins like map, filter, setTimeout and addEventListener are higher-order functions. Too many nested callbacks lead to "callback hell", which is why promises and async/await are preferred for asynchronous code.
23. What is the difference between the for...in and for...of loops?
for...in iterates over the enumerable property keys of an object, including inherited ones, while for...of iterates over the values of any iterable such as arrays, strings, maps, sets and generators. Use for...of for arrays, because for...in gives string indexes and may include extra properties. Plain objects are not iterable, so use Object.keys(), Object.values() or Object.entries() with for...of.
const arr = ["a", "b"];
for (const i in arr) console.log(i);
for (const v of arr) console.log(v);
24. What are template literals and what ES6+ features should every developer know?
Template literals are strings written with backticks that support embedded expressions through ${}, multi-line text, and tagged templates. Beyond them, the most important ES6+ features are let/const, arrow functions, destructuring, spread/rest, default parameters, classes, modules (import/export), promises, and Map/Set. Later versions added async/await, optional chaining (?.), nullish coalescing (??), Object.entries(), and Array.prototype.at().
const user = { name: "Ana", address: null };
const city = user.address?.city ?? "unknown";
console.log(`Hello ${user.name}, city: ${city}`);
25. What is the difference between synchronous and asynchronous code, and what is debouncing?
Synchronous code runs line by line and blocks the thread until each step finishes, while asynchronous code starts a long operation (network, timer, file) and lets other code run until the result is ready. JavaScript is single-threaded, so async behavior is handled by the event loop and callbacks, promises or async/await. Debouncing is a related technique that delays a function until a pause in calls, for example waiting until a user stops typing before sending a search request.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
26. What is the output of a loop that creates setTimeout callbacks with var versus let?
With var the callbacks print the same final value, while with let each iteration gets its own binding and prints the expected value. var is function-scoped, so all callbacks close over one shared variable that has already reached its end value when the timers fire. let creates a fresh binding per iteration. The older fix was wrapping the body in an IIFE, but let is the standard solution now.
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j), 0);
27. What is memoization and how would you implement it?
Memoization is an optimization that caches the result of a function for given arguments, so repeated calls with the same input return the stored value instead of recomputing. It only works safely for pure functions. A closure holds a Map as the cache, and the arguments are turned into a key. Watch for unbounded cache growth, and for keys that collide or fail when arguments are objects.
function memoize(fn) {
const cache = new Map();
return (arg) => {
if (!cache.has(arg)) cache.set(arg, fn(arg));
return cache.get(arg);
};
}
28. What is currying and how is it different from partial application?
Currying transforms a function that takes several arguments into a chain of functions that each take one argument, while partial application fixes some arguments up front and returns a function that takes the rest. f(a, b, c) becomes f(a)(b)(c) when curried. Currying makes functions easy to reuse and compose, for example creating specialized helpers. bind gives you partial application natively.
const add = (a) => (b) => a + b;
const add5 = add(5);
add5(3);
29. What is throttling and how does it differ from debouncing?
Throttling guarantees a function runs at most once per fixed time interval, while debouncing waits until calls stop for a given delay before running once. Use throttling for continuous events where regular updates are wanted, such as scroll, resize or mouse move handlers. Use debouncing when only the final value matters, such as search-as-you-type. Both reduce expensive work and are built with closures and timers.
function throttle(fn, ms) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= ms) { last = now; fn(...args); }
};
}
30. What are generators and iterators in JavaScript?
An iterator is an object with a next() method that returns { value, done }, and a generator is a function declared with function* that produces an iterator and can pause at each yield. Generators run lazily, so they are good for infinite sequences, custom iteration and streaming large data without building a whole array. Any object with a [Symbol.iterator] method works with for...of and spread.
function* count() {
let n = 0;
while (true) yield n++;
}
const it = count();
it.next().value;
it.next().value;
31. What are Map and Set, and when would you use them instead of objects and arrays?
Map is a key-value collection that allows keys of any type and remembers insertion order, and Set stores unique values. Use a Map instead of a plain object when keys are not strings, when you add and remove entries often, or when you need an exact size. Use a Set to remove duplicates or to check membership quickly with has(). Both are iterable and avoid prototype key collisions like "constructor".
const unique = [...new Set([1, 2, 2, 3])];
const m = new Map([[{ id: 1 }, "a"]]);
m.size;
32. What are WeakMap and WeakSet, and what problem do they solve?
WeakMap and WeakSet hold their keys or values weakly, so an entry does not prevent the object from being garbage collected once nothing else references it. Their keys must be objects, they are not iterable, and they have no size. They solve memory leaks when you attach metadata or private data to objects you do not own, such as caching per DOM element. When the element is removed, the cache entry disappears automatically.
33. What are common causes of memory leaks in JavaScript?
Memory leaks happen when objects that are no longer needed stay reachable, so the garbage collector cannot free them. Typical causes are forgotten timers and intervals, event listeners that are never removed, accidental globals, closures that hold large data, detached DOM nodes still referenced in variables, and ever-growing caches. Fix them by clearing timers, calling removeEventListener or using AbortController, and using WeakMap for caches. Browser DevTools heap snapshots help you find them.
34. What is event delegation and why is it useful?
Event delegation means attaching one listener to a parent element and handling events from its children, using the fact that most events bubble up the DOM. It uses less memory than adding a listener to every child, and it automatically works for elements added later. Inside the handler you check event.target, usually with closest(), to find which child was clicked.
list.addEventListener("click", (e) => {
const item = e.target.closest("li");
if (item) console.log(item.textContent);
});
35. What is the difference between event bubbling and capturing, and what do stopPropagation and preventDefault do?
In capturing the event travels from the window down to the target, and in bubbling it travels from the target back up to the window; listeners run during bubbling by default unless you pass { capture: true }. stopPropagation() stops the event from moving further along that path, while preventDefault() cancels the browser's default action, such as following a link or submitting a form. They are independent, and you often need only one of them.
36. What is the difference between localStorage, sessionStorage and cookies?
localStorage keeps data with no expiry until it is cleared, sessionStorage keeps it only for the lifetime of one tab, and cookies are small pieces of data (about 4 KB) that are sent to the server with every matching request. Web storage offers around 5 MB, is synchronous, and is not sent to the server. Never store tokens or secrets in localStorage if XSS is a risk, because any script can read it. Prefer HttpOnly cookies for session tokens.
37. What is CORS and why does the browser block some requests?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that lets a server declare which other origins may read its responses, using headers like Access-Control-Allow-Origin. Without permission, the browser blocks JavaScript from reading a cross-origin response, which protects users from malicious sites. Non-simple requests trigger a preflight OPTIONS request first. The fix is always on the server, not in the client code, and CORS does not protect the server itself.
38. What is the difference between fetch and XMLHttpRequest, and why does fetch not reject on a 404?
fetch is the modern promise-based API for network requests, while XMLHttpRequest is an older callback-based one. fetch only rejects its promise on network failures, and an HTTP error such as 404 or 500 still resolves, so you must check response.ok yourself. It also does not send cookies cross-origin by default and has no built-in timeout, so use AbortController for cancelling.
const res = await fetch("/api/data");
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
39. What is the difference between ES modules and CommonJS?
ES modules use import and export, are statically analyzable, load asynchronously, and are the official standard that works in browsers and Node.js. CommonJS uses require() and module.exports, loads synchronously, and is the older Node.js format. Static structure lets bundlers do tree shaking to remove unused code. ES modules export live bindings, whereas CommonJS exports a copy of the value, and ES modules support top-level await.
40. What are Symbols and what are they used for?
A Symbol is a primitive that is guaranteed to be unique, created with Symbol("description"), even when two symbols have the same description. They are mainly used as property keys that cannot collide with other keys, which is useful for hidden metadata or library internals. Symbol keys are skipped by for...in and Object.keys(). Well-known symbols such as Symbol.iterator let objects customize built-in language behavior.
41. What is a Proxy in JavaScript and where would you use it?
A Proxy wraps an object and lets you intercept operations such as reading, writing, deleting properties or calling functions, through handler functions called traps. It is used for validation, logging, default values, and reactive state, which is how some frontend frameworks detect changes. Combined with Reflect, you can forward the default behavior inside a trap. Proxies add overhead, so avoid them in performance-critical hot paths.
const p = new Proxy({}, {
get: (t, k) => (k in t ? t[k] : "default"),
});
p.x;
42. What are the pitfalls of using sort() on an array of numbers?
Array.prototype.sort() converts elements to strings by default and sorts them in UTF-16 order, so [10, 9, 1].sort() returns [1, 10, 9]. Pass a comparator to sort numbers correctly. Also note that sort() mutates the original array; use toSorted() or copy first with [...arr] when you need to keep the original.
[10, 9, 1].sort((a, b) => a - b);
const copy = [10, 9, 1].toSorted((a, b) => a - b);
43. How do you handle floating-point errors like 0.1 + 0.2 !== 0.3?
JavaScript numbers are 64-bit binary floating-point values, so many decimals such as 0.1 cannot be stored exactly and 0.1 + 0.2 gives 0.30000000000000004. Compare with a tolerance using Math.abs(a - b) < Number.EPSILON, or round for display with toFixed(). For money, store integer cents or use a decimal library, and use BigInt for integers beyond Number.MAX_SAFE_INTEGER.
44. What is the difference between Object.freeze and const, and how would you make an object deeply immutable?
const prevents reassigning the variable, while Object.freeze() prevents adding, deleting or changing properties of the object itself. Freezing is shallow, so nested objects can still be modified. For deep immutability, recursively freeze each nested object, or follow an immutable update style using spread and structuredClone. Frozen objects silently ignore writes in sloppy mode and throw a TypeError in strict mode.
const o = Object.freeze({ a: 1, inner: { b: 2 } });
o.a = 5;
o.inner.b = 9;
45. What happens when you throw an error inside a Promise chain, and what is an unhandled rejection?
A thrown error inside a .then() callback turns the returned promise into a rejected one, and it skips to the next .catch() in the chain. If no handler is attached to a rejected promise, it becomes an unhandled rejection, which triggers the unhandledrejection event in browsers and can crash a Node.js process. Always end a chain with .catch(), or use try/catch with await, and remember to return inner promises so their errors propagate.
46. How would you run async tasks in sequence versus in parallel, and what is the mistake with forEach and await?
For sequential execution use a for...of loop with await; for parallel execution create all promises first and await them with Promise.all. The common mistake is using await inside forEach, because forEach ignores returned promises and does not wait for them. Also avoid awaiting inside a loop when tasks are independent, since it makes them needlessly slow.
for (const id of ids) await save(id);
await Promise.all(ids.map((id) => save(id)));
47. What is the difference between microtasks and macrotasks, and what happens if a microtask keeps scheduling more microtasks?
Microtasks, such as promise callbacks and queueMicrotask, run right after the current script finishes and before the browser renders or the next macrotask runs, while macrotasks, such as setTimeout and UI events, run one per loop turn. The microtask queue is drained completely, so a microtask that keeps scheduling new microtasks starves the event loop and freezes rendering and timers. Break heavy work into macrotasks or use requestAnimationFrame.
48. How would you optimize a slow JavaScript page or long-running computation?
First measure with the browser Performance panel, then fix the real bottleneck. Common wins are avoiding repeated layout reads and writes in the DOM, batching updates, debouncing or throttling handlers, virtualizing long lists, lazy loading code with dynamic import(), and caching results. For CPU-heavy work, move it to a Web Worker so the main thread stays responsive. Avoid premature micro-optimizations that do not show up in profiling.
49. What are the most common JavaScript security issues, and how do you prevent them?
The most common issue is cross-site scripting (XSS), where untrusted input is rendered as HTML or script. Prevent it by using textContent instead of innerHTML, escaping or sanitizing user content, and setting a Content Security Policy. Other risks are CSRF, solved with SameSite cookies and tokens, prototype pollution through unsafe object merging, and using eval() or new Function() on user data. Never trust client-side validation alone, and keep dependencies updated.
el.textContent = userInput;
el.innerHTML = userInput;
50. What are best practices for writing clean, maintainable JavaScript?
Use const by default and let when needed, and always use strict equality. Keep functions small and single-purpose, name things clearly, and avoid mutating inputs or relying on global state. Handle errors explicitly instead of swallowing them, prefer async/await over nested callbacks, and use ES modules to organize code. Add a linter such as ESLint and a formatter like Prettier, write tests for important logic, and consider TypeScript for larger codebases.