Skip to content
Frontend

45 JavaScript Interview Questions and Answers

This focused guide turns RecallDeck’s curated JavaScript material into 45 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

56 min read45 detailed answersReviewed Aug 24, 2026
What to remember

Connect every framework answer to browser behavior, accessibility, performance, or user-visible state; that is where senior frontend judgment becomes visible.

Question set

45 detailed answers

01

1. Data types: primitives vs objects?

Short answer: JS has 7 primitive types (string, number, boolean, null, undefined, symbol, bigint) and one reference type — the object (including arrays, functions, dates). Primitives are immutable and copied by value; objects are copied by reference.

In depth:

// Primitives — copied by value
let a = 10;
let b = a;
b = 20;
console.log(a); // 10 — a is unchanged

// Objects — copied by reference
let obj1 = { x: 1 };
let obj2 = obj1;
obj2.x = 99;
console.log(obj1.x); // 99 — both variables point to the same object

// Primitives are immutable: you can't "change" a string, only create a new one
let str = "hello";
str[0] = "H";
console.log(str); // "hello" — unchanged

// bigint — for integers larger than 2^53 - 1
const big = 9007199254740993n;
console.log(big + 1n); // 9007199254740994n

// symbol — a unique identifier
const id = Symbol("id");

Primitives store the value directly. They "have methods" thanks to autoboxing: "abc".toUpperCase() temporarily wraps the string in a String wrapper object.

⚠️ Gotcha: typeof function(){} returns 'function', even though a function is an object. And typeof [] is 'object', so for arrays use Array.isArray([]).

02

2. typeof and the typeof null bug?

Short answer: typeof returns a string with the type's name. The well-known bug: typeof null === 'object', even though null is a primitive.

In depth:

typeof 42;            // "number"
typeof "str";         // "string"
typeof true;          // "boolean"
typeof undefined;     // "undefined"
typeof Symbol();      // "symbol"
typeof 10n;           // "bigint"
typeof {};            // "object"
typeof [];            // "object"  (!)
typeof function(){};  // "function"
typeof null;          // "object"  ← bug

typeof null === 'object' is a historical mistake from the very first version of JS (1995). In the value representation, the type was stored in the low bits; objects had the tag 000, and null was a null pointer, also 000. It can't be fixed — too much code would break.

// Correct null check
function isNull(v) { return v === null; }

// Check for a "real" object (not null, not an array)
function isPlainObject(v) {
  return typeof v === "object" && v !== null && !Array.isArray(v);
}

⚠️ Gotcha: typeof undeclaredVariable does NOT throw a ReferenceError, it returns 'undefined'. This is the only safe way to check an undeclared variable.

03

3. null vs undefined?

Short answer: undefined — a variable is declared but no value has been assigned (the default). null — an explicit "empty value" that the programmer sets deliberately.

In depth:

let x;
console.log(x); // undefined — not initialized

let y = null;
console.log(y); // null — explicitly "nothing"

// undefined arises on its own:
function f(a) { return a; }
f();                  // undefined — no argument passed
({}).noSuchProp;      // undefined — no such property
[].length === 0;      // it has a length, but [5] doesn't → arr[5] === undefined

// Comparisons
null == undefined;   // true  (loose — a special rule)
null === undefined;  // false (different types)

typeof undefined;    // "undefined"
typeof null;         // "object"

// JSON.stringify treats them differently
JSON.stringify({ a: undefined, b: null }); // '{"b":null}' — undefined is dropped

Convention: assign null yourself when you want to say "there's deliberately no value"; leave undefined to the engine.

⚠️ Gotcha: Default values in parameters and destructuring only kick in for undefined, NOT for null:

function g(a = 5) { return a; }
g(undefined); // 5
g(null);      // null — the default didn't apply!
04

4. == vs ===?

Short answer: === (strict) compares without type coercion — the types must match. == (loose) coerces types before comparing, which produces many non-obvious results.

In depth:

1 === "1";   // false — different types
1 == "1";    // true  — the string is coerced to a number

// Table of == surprises
0 == "";        // true   ("" → 0)
0 == "0";       // true   ("0" → 0)
0 == false;     // true   (false → 0)
"" == false;    // true   (both → 0)
null == undefined; // true (special rule)
null == 0;      // false  (null isn't coerced to a number here)
NaN == NaN;     // false  (NaN equals nothing, not even itself)
[] == ![];      // true   (see the breakdown below)
[] == "";       // true   ([] → "")
[] == 0;        // true   ([] → "" → 0)
[null] == 0;    // true
[0] == false;   // true

// Breakdown of [] == ![]:
// 1) ![] → false (the array is truthy, negation → false)
// 2) [] == false
// 3) false → 0, [] → "" → 0
// 4) 0 == 0 → true

// NaN is checked like this:
Number.isNaN(NaN); // true
NaN !== NaN;       // true — the hack check for NaN

When is == appropriate? Practically always use ===. The only justified use of == is the check x == null, which catches both null and undefined in one operation:

if (x == null) { /* x is null or undefined */ }

⚠️ Gotcha: == isn't transitive: "" == 0 (true) and "0" == 0 (true), but "" == "0" (false). So you can't rely on == for chains.

05

5. Type coercion and truthy/falsy?

Short answer: Coercion is automatic type conversion. In a boolean context there are exactly 8 falsy values; everything else is truthy.

In depth:

The list of falsy values (coerced to false):

false
0, -0, 0n      // zeros
""             // empty string
null
undefined
NaN

Everything else is truthy, including the traps:

Boolean("0");      // true  — a non-empty string
Boolean("false");  // true  — a non-empty string
Boolean([]);       // true  — an empty array is truthy!
Boolean({});       // true  — an empty object is truthy!
Boolean(" ");      // true  — a space is non-empty
Boolean(-1);       // true  — any nonzero number

Kinds of coercion:

// To string (the + operator, if one operand is a string)
1 + "2";       // "12"
"5" + 3;       // "53"

// To number (arithmetic other than +)
"5" - 1;       // 4
"5" * 2;       // 10
+"42";         // 42
+"";           // 0
+"abc";        // NaN

// To boolean (if, ||, &&, !)
if ([]) console.log("array is truthy"); // runs

⚠️ Gotcha: [] + []"" (empty string), [] + {}"[object Object]", {} + []0 in some contexts (if {} is treated as a code block). Avoid arithmetic with objects/arrays.

06

6. var vs let vs const?

Short answer: var — function scope, hoisting with undefined, can be redeclared. let/const — block scope, temporal dead zone, can't be redeclared. const forbids reassigning the binding.

In depth:

Property var let const
Scope function block {} block {}
Hoisting yes, = undefined yes, but TDZ yes, but TDZ
Reassignment yes yes no
Redeclaration yes no no
Global property yes (in the browser) no no
// Block vs function scope
function scope() {
  if (true) {
    var v = "var";
    let l = "let";
  }
  console.log(v); // "var" — visible outside the block
  console.log(l); // ReferenceError — l doesn't exist
}

// Temporal Dead Zone (TDZ)
console.log(a); // undefined (var is hoisted)
console.log(b); // ReferenceError: Cannot access 'b' before initialization
var a = 1;
let b = 2;

// const forbids reassignment, but not mutating the object
const obj = { x: 1 };
obj.x = 2;       // OK — mutating the contents
obj = {};        // TypeError — reassigning the binding
const arr = [1];
arr.push(2);     // OK

⚠️ Gotcha: const does NOT make the value immutable — it only freezes the binding. The contents of an object/array can still be changed. For immutability you need Object.freeze.

07

7. Hoisting?

Short answer: Variable and function declarations are "raised" to the top of their scope at the compilation stage. var is initialized to undefined, a function declaration is hoisted in full, and let/const land in the TDZ.

In depth:

// How we write it          // How the engine sees it
console.log(x);          var x;        // declaration goes to the top
var x = 5;               console.log(x); // undefined
                         x = 5;

// A function declaration is hoisted in full
sayHi(); // "Hi" — works before the declaration
function sayHi() { console.log("Hi"); }

// A function expression is NOT hoisted (only the variable is)
sayBye(); // TypeError: sayBye is not a function
var sayBye = function() { console.log("Bye"); };
// var sayBye was hoisted as undefined → undefined() → error

// let/const in the TDZ
{
  // the TDZ for name begins here
  console.log(name); // ReferenceError
  let name = "Anna";  // the TDZ ends
}

Precedence order during hoisting: a function declaration overrides a var with the same name.

⚠️ Gotcha: A function declaration inside an if block behaves differently in strict/non-strict mode and across engines. Inside blocks, declare functions via let/const + an arrow/expression.

08

8. Closures?

Short answer: A closure is a function together with the lexical environment in which it was created. The function "remembers" the variables of the outer scope even after the outer function has finished.

In depth:

// A counter — the classic example
function makeCounter() {
  let count = 0; // private variable
  return function () {
    count++;
    return count;
  };
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count lives between calls, even though makeCounter has finished

// Private variables (encapsulation)
function createBankAccount(initial) {
  let balance = initial; // not directly accessible from outside
  return {
    deposit(sum) { balance += sum; return balance; },
    getBalance() { return balance; },
  };
}
const acc = createBankAccount(100);
acc.deposit(50);          // 150
console.log(acc.getBalance()); // 150
console.log(acc.balance); // undefined — no direct access

// A function factory
function multiplier(factor) {
  return (n) => n * factor; // closes over factor
}
const double = multiplier(2);
console.log(double(5)); // 10

A closure is created when the function is created, not when it's called. Each call of the outer function spawns a new, independent environment.

⚠️ Gotcha: Closures hold references to variables → they can cause memory leaks if a closure lives long and retains large objects. Functions created in a loop with var also share one binding, so they all observe its final value after the loop. let creates a separate lexical binding for each iteration.

09

9. The var-in-a-loop and setTimeout trap?

Short answer: With var, all callbacks close over the same variable, so after the loop they see its final value. With let, each iteration creates a new binding.

In depth:

// The problem with var
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3
// var i is a single variable for the whole loop. By the time the
// timers fire (after the loop), i is already 3.

// Fix 1: let — block scope, a new binding per iteration
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2

// Fix 2: an IIFE, capture the value by copy
for (var i = 0; i < 3; i++) {
  (function (j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}
// Output: 0, 1, 2

// Fix 3: the second argument of setTimeout
for (var i = 0; i < 3; i++) {
  setTimeout((j) => console.log(j), 100, i);
}
// Output: 0, 1, 2

⚠️ Gotcha: Even with let, the trap returns if you declare the variable OUTSIDE the loop: let i; for (i = 0; ...). A per-iteration binding is created only when let is in the for header itself.

10

10. this: the 4 binding rules?

Short answer: The value of this is determined at the moment the function is CALLED, by 4 precedence rules: 1) new 2) explicit binding (call/apply/bind) 3) object method 4) plain call (default).

In depth:

// 1. Plain call (default binding)
function show() { console.log(this); }
show();
// non-strict: the global object (window/global)
// strict mode: undefined

// 2. Object method (implicit binding) — this = the object to the left of the dot
const user = {
  name: "Anna",
  greet() { console.log(this.name); },
};
user.greet(); // "Anna" — this = user

// 3. new (new binding) — this = the new object
function User(name) { this.name = name; }
const u = new User("Ivan");
console.log(u.name); // "Ivan"

// 4. Explicit binding
function say() { console.log(this.role); }
const admin = { role: "admin" };
say.call(admin);  // "admin"
say.apply(admin); // "admin"
const bound = say.bind(admin);
bound();          // "admin"

Precedence (highest to lowest): new > bind/call/apply > object method > plain call.

Important: this is determined NOT by where the function is declared, but by HOW it was called (except for arrow functions).

⚠️ Gotcha: "Losing" the context when passing a method as a reference:

const greet = user.greet;
greet(); // this = undefined/window → error or undefined
// The method is detached from the object → the plain-call rule applies.
11

11. call / apply / bind?

Short answer: All three set this explicitly. call invokes immediately with comma-separated arguments, apply — with an array of arguments, bind returns a new function with a fixed this (without invoking).

In depth:

function introduce(greeting, punct) {
  return `${greeting}, I'm ${this.name}${punct}`;
}
const person = { name: "Lena" };

// call — arguments listed out
introduce.call(person, "Hi", "!");   // "Hi, I'm Lena!"

// apply — arguments as an array
introduce.apply(person, ["Hello", "."]); // "Hello, I'm Lena."

// bind — returns a new function, doesn't invoke
const boundIntro = introduce.bind(person, "Hey");
boundIntro("?"); // "Hey, I'm Lena?"

// Practice: bind to preserve this in a callback
class Timer {
  constructor() { this.seconds = 0; }
  start() {
    setInterval(function () {
      this.seconds++; // without bind, this is lost
    }.bind(this), 1000);
  }
}

// apply for max/min of an array (before spread)
Math.max.apply(null, [1, 5, 3]); // 5
Math.max(...[1, 5, 3]);          // 5 — the modern equivalent

// Borrowing methods
const arrayLike = { 0: "a", 1: "b", length: 2 };
Array.prototype.join.call(arrayLike, "-"); // "a-b"

Mnemonic: Apply — Array, Call — Comma.

⚠️ Gotcha: bind creates a "hard" binding — a second bind or even a new call generally won't override an already-bound this (although new on top of a bound function is a special case that ignores the bound this but uses the bound arguments). Also, bind returns a NEW function each time, so removeEventListener with a bound handler won't work if the bind was done anew.

12

12. Arrow functions vs regular ones?

Short answer: Arrow functions have no this of their own (they take it lexically from the surrounding scope), have no arguments, can't be called with new, have no prototype, and aren't hoisted as declarations.

In depth:

// this is lexical (from where it's declared, not called)
const obj = {
  name: "Tom",
  regular() {
    setTimeout(function () {
      console.log(this.name); // undefined — its own this
    }, 100);
    setTimeout(() => {
      console.log(this.name); // "Tom" — this from regular
    }, 100);
  },
};

// No arguments of its own
function regular() { return arguments; }   // works
const arrow = () => arguments;             // error/outer arguments
// Instead — rest:
const arrowRest = (...args) => args;

// Can't be used as a constructor
const Arrow = () => {};
new Arrow(); // TypeError: Arrow is not a constructor

// No prototype
console.log(Arrow.prototype); // undefined

// Implicit return
const sum = (a, b) => a + b;          // no return
const makeObj = () => ({ x: 1 });     // an object in parentheses!

When NOT to use arrow functions: object methods (which need a dynamic this), prototype methods, event handlers where you need this = element, constructor functions.

⚠️ Gotcha: An object method as an arrow breaks this:

const counter = {
  count: 0,
  inc: () => { this.count++; }, // this is the outer one (window/undefined), not counter!
};
13

13. Losing this in callbacks?

Short answer: When a method is passed as a callback (to setTimeout, an event handler, map), it's invoked "detached" from the object and this is lost. Solutions: an arrow function, bind, saving this in a variable.

In depth:

class Button {
  constructor(label) {
    this.label = label;
  }
  // Problem: this is lost when passed
  handleClickBroken() {
    console.log(this.label); // this = element, not Button
  }
  // Fix 1: bind in the constructor
  // this.handleClick = this.handleClick.bind(this);

  // Fix 2: an arrow class field (lexical this)
  handleClick = () => {
    console.log(this.label); // always the Button instance
  };
}

const btn = new Button("OK");
element.addEventListener("click", btn.handleClick); // works (arrow field)

// In arrays: the second argument thisArg
const obj = {
  prefix: ">> ",
  items: ["a", "b"],
  render() {
    // forEach accepts thisArg as its second parameter
    this.items.forEach(function (i) {
      console.log(this.prefix + i);
    }, this); // ← pass this
    // or just an arrow:
    this.items.forEach((i) => console.log(this.prefix + i));
  },
};
obj.render();

⚠️ Gotcha: setTimeout(this.method, 1000) loses the context. You need setTimeout(() => this.method(), 1000) or setTimeout(this.method.bind(this), 1000).

14

14. Prototypes and prototypal inheritance?

Short answer: Every object has an internal reference [[Prototype]] (accessible as __proto__) to another object. When you access a property, the engine searches for it up the prototype chain. prototype is a property of constructor functions that becomes the prototype of the objects they create.

In depth:

// prototype (on the constructor) vs __proto__ (on the instance)
function Animal(name) { this.name = name; }
Animal.prototype.speak = function () {
  return `${this.name} makes a sound`;
};

const cat = new Animal("Cat");
console.log(cat.speak());            // "Cat makes a sound"
console.log(cat.__proto__ === Animal.prototype); // true
console.log(Object.getPrototypeOf(cat) === Animal.prototype); // true

// The prototype chain: cat → Animal.prototype → Object.prototype → null
console.log(cat.toString()); // found in Object.prototype

// Property lookup goes up the chain
const parent = { greet() { return "hello"; } };
const child = Object.create(parent); // child.__proto__ = parent
child.name = "child";
console.log(child.greet());          // "hello" — found in parent
console.log(child.hasOwnProperty("greet")); // false — not its own
console.log(child.hasOwnProperty("name"));  // true

// Object.create — creating with a given prototype
const proto = { type: "base" };
const o = Object.create(proto);
console.log(o.type); // "base"

The chain diagram for cat:

cat ──__proto__──▶ Animal.prototype ──__proto__──▶ Object.prototype ──__proto__──▶ null

⚠️ Gotcha: __proto__ is a getter/setter from Object.prototype, formally deprecated. Use Object.getPrototypeOf / Object.setPrototypeOf. Changing the prototype of an existing object (Object.setPrototypeOf) badly hurts performance.

15

15. The new operator under the hood?

Short answer: new does 4 things: creates an empty object, links its prototype to the constructor's prototype, calls the constructor with this = the new object, and returns the object (unless the constructor returned its own).

In depth:

function User(name) {
  this.name = name;
}
const u = new User("Anya");

// What happens under the hood — an emulation:
function myNew(Constructor, ...args) {
  // 1. Create an empty object
  const obj = {};
  // 2. Link the prototype
  Object.setPrototypeOf(obj, Constructor.prototype);
  // 3. Call the constructor with this = obj
  const result = Constructor.apply(obj, args);
  // 4. Return obj, unless the constructor returned an object
  return typeof result === "object" && result !== null ? result : obj;
}

const u2 = myNew(User, "Bob");
console.log(u2.name); // "Bob"

// If the constructor returns an object — it overrides this
function Weird() {
  this.a = 1;
  return { b: 2 };
}
console.log(new Weird()); // { b: 2 } — a primitive return is ignored

⚠️ Gotcha: Forgot new — in non-strict mode this becomes the global object and the constructor "pollutes" it. Protection: a class throws a TypeError without new, or a check if (!(this instanceof User)) return new User(...).

16

16. class — syntactic sugar over prototypes?

Short answer: class is syntactic sugar over constructor functions and prototypes. Class methods are placed on the prototype; inheritance via extends sets up the prototype chain.

In depth:

class Animal {
  constructor(name) {
    this.name = name; // instance properties
  }
  speak() {           // goes onto Animal.prototype
    return `${this.name} speaks`;
  }
  static create(name) { // a static method — on the class itself
    return new Animal(name);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name);      // call the parent constructor
  }
  speak() {
    return super.speak() + " woof"; // super — access to the parent
  }
}

const d = new Dog("Rex");
console.log(d.speak()); // "Rex speaks woof"

// Proof that it's prototypes:
console.log(typeof Animal);                     // "function"
console.log(d.speak === Dog.prototype.speak);   // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true

Differences from regular functions: a class isn't hoisted (TDZ), is always in strict mode, can't be called without new, and its methods are non-enumerable.

⚠️ Gotcha: Private fields #field are truly private (syntactically), unlike the _field convention. Accessing #field from outside is a syntax error, not undefined.

17

17. Event loop?

Short answer: JS is single-threaded. The event loop constantly checks: if the call stack is empty, it takes a task from the queue. First the ENTIRE microtask queue (promises) is drained, then one macrotask (setTimeout, events) is taken, then all microtasks again.

In detail:

Components:

  • Call Stack — the stack of synchronous code calls.
  • Web APIs — browser APIs (timers, fetch, DOM events) run outside the engine.
  • Callback Queue (macrotask)setTimeout, setInterval, events, setImmediate.
  • Microtask QueuePromise.then/catch/finally, queueMicrotask, MutationObserver.

The algorithm of a single "tick":

  1. Run all synchronous code (drain the stack).
  2. Drain the ENTIRE microtask queue (including ones added along the way).
  3. Render (in the browser, if needed).
  4. Take ONE macrotask, run it.
  5. Repeat from step 2.
console.log("1 — synchronous");

setTimeout(() => console.log("2 — macrotask"), 0);

Promise.resolve().then(() => console.log("3 — microtask"));

console.log("4 — synchronous");

// Output:
// 1 — synchronous
// 4 — synchronous
// 3 — microtask   ← microtasks before macrotasks
// 2 — macrotask

⚠️ Gotcha: Endlessly adding microtasks (e.g. a recursive Promise.then or queueMicrotask) will block rendering and macrotasks — the microtask queue is fully drained before any macrotask.

18

18. Microtask vs macrotask — what is the output order?

Short answer: Microtasks (promises) always run before macrotasks (timers) after each synchronous block. This is a common "guess the order" question.

In detail:

console.log("start");

setTimeout(() => console.log("timeout 1"), 0);

Promise.resolve()
  .then(() => console.log("promise 1"))
  .then(() => console.log("promise 2"));

setTimeout(() => console.log("timeout 2"), 0);

console.log("end");

// Output:
// start
// end
// promise 1
// promise 2
// timeout 1
// timeout 2

A more complex example with async/await:

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end"); // equivalent to .then → microtask
}
async function async2() {
  console.log("async2");
}

console.log("script start");
setTimeout(() => console.log("setTimeout"), 0);
async1();
Promise.resolve().then(() => console.log("promise"));
console.log("script end");

// Output:
// script start
// async1 start
// async2
// script end
// async1 end   ← code after await = microtask
// promise
// setTimeout

⚠️ Gotcha: Code after await is queued as a microtask, so it runs AFTER all synchronous code of the current block, but BEFORE any setTimeout. await async2() first runs the body of async2 synchronously, and only the continuation is deferred.

19

19. Promises: states and methods?

Short answer: A promise is an object representing the result of an asynchronous operation. Three states: pendingfulfilled (resolve) or rejected (reject). The transition is irreversible. Methods: then, catch, finally.

In detail:

const promise = new Promise((resolve, reject) => {
  const ok = true;
  if (ok) resolve("success");
  else reject(new Error("error"));
});

promise
  .then((value) => {
    console.log(value); // "success"
    return value.toUpperCase(); // return → next then
  })
  .then((upper) => console.log(upper)) // "SUCCESS"
  .catch((err) => console.error(err))  // catches any error above
  .finally(() => console.log("done")); // always runs

// Chains: each then returns a new promise
fetchUser()
  .then((user) => fetchPosts(user.id)) // return a promise → wait for it
  .then((posts) => console.log(posts))
  .catch((err) => console.error("Any error in the chain:", err));

// States are irreversible
const p = new Promise((resolve, reject) => {
  resolve("first");
  resolve("second"); // ignored
  reject("error");   // ignored
});

catch catches errors from ALL preceding thens. finally does not receive the value and does not change it (but if it throws/returns a reject, it does affect the result).

⚠️ Gotcha: Errors thrown inside a then go to the nearest following catch, but NOT to a catch placed BEFORE them. Also: an uncaught rejected promise triggers unhandledrejection. A value returned from then is automatically wrapped in a resolved promise.

20

20. Promise.all / allSettled / race / any?

Short answer: all — waits for all, fails on the first error. allSettled — waits for all, never fails. race — the first to settle (any outcome). any — the first to succeed.

In detail:

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.reject("error");

// all — array of results or the first error
Promise.all([p1, p2])
  .then((res) => console.log(res)); // [1, 2]
Promise.all([p1, p3])
  .catch((err) => console.log(err)); // "error" — the whole all failed

// allSettled — status of each, never fails
Promise.allSettled([p1, p3]).then((res) => console.log(res));
// [
//   { status: "fulfilled", value: 1 },
//   { status: "rejected", reason: "error" }
// ]

// race — the first to settle (success OR error)
Promise.race([
  new Promise((r) => setTimeout(() => r("slow"), 200)),
  new Promise((r) => setTimeout(() => r("fast"), 100)),
]).then((res) => console.log(res)); // "fast"

// any — the first SUCCESSFUL one, ignores errors
Promise.any([p3, p1]).then((res) => console.log(res)); // 1
// if all fail → AggregateError
Method When it resolves On error
all all succeed fails immediately on the first
allSettled all settle never fails
race the first to settle fails if the first one failed
any the first to succeed fails only if all failed

⚠️ Gotcha: Promise.all stops on the first error, but the remaining promises are NOT cancelled — they keep running (promises cannot be cancelled at all). For fault tolerance (when you need all results regardless), use allSettled.

21

21. async/await?

Short answer: async/await is syntactic sugar over promises. An async function always returns a promise; await pauses it until the promise resolves. Errors are caught with try/catch.

In detail:

// an async function always returns a promise
async function getValue() {
  return 42; // wrapped in Promise.resolve(42)
}
getValue().then((v) => console.log(v)); // 42

// await "unwraps" the promise
async function loadUser() {
  const response = await fetch("/api/user"); // wait
  const user = await response.json();        // wait
  return user;
}

// Error handling via try/catch
async function safeLoad() {
  try {
    const user = await loadUser();
    console.log(user);
  } catch (err) {
    console.error("Load error:", err); // catches reject and throw
  } finally {
    console.log("finished");
  }
}

// Equivalent with promises:
function loadUserPromise() {
  return fetch("/api/user").then((r) => r.json());
}

await can only be used inside async (or at the top level of a module — top-level await).

⚠️ Gotcha: A forgotten await returns a promise instead of a value:

async function bug() {
  const data = loadUser(); // forgot await
  console.log(data); // Promise { <pending> }, not an object
}

And try/catch will NOT catch the error if there is no await before the promise.

22

22. Parallel vs sequential await execution?

Short answer: Several awaits in a row run sequentially (slow). To run independent operations in parallel, use Promise.all.

In detail:

// BAD: sequential — 3 seconds total
async function sequential() {
  const a = await fetchA(); // 1 sec, wait
  const b = await fetchB(); // another 1 sec
  const c = await fetchC(); // another 1 sec
  return [a, b, c];
}

// GOOD: parallel — ~1 second (max of the three)
async function parallel() {
  const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
  return [a, b, c];
}

// In-between option: start, then await
async function startThenAwait() {
  const pA = fetchA(); // start without await — operations begin immediately
  const pB = fetchB();
  const a = await pA;  // now wait
  const b = await pB;
  return [a, b];
}

Sequential await is justified only when the operations are dependent (the result of one is needed for the next).

⚠️ Gotcha: await in a for...of loop is sequential per iteration:

// Slow — one at a time
for (const url of urls) {
  await fetch(url);
}
// Fast — all at once
await Promise.all(urls.map((url) => fetch(url)));

Sometimes the sequencing is intentional (rate limiting), but more often it is an accidental slowdown.

23

23. Callbacks and callback hell?

Short answer: A callback is a function passed into another function to be called later. Nesting many asynchronous callbacks produces "callback hell" — an unreadable pyramid. It is solved with promises and async/await.

In detail:

// Callback hell — the "pyramid of doom"
getUser(1, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      getAuthor(comments[0].id, (author) => {
        console.log(author);
        // error handling? separately at every level...
      }, handleError);
    }, handleError);
  }, handleError);
}, handleError);

// The same with promises — flat
getUser(1)
  .then((user) => getPosts(user.id))
  .then((posts) => getComments(posts[0].id))
  .then((comments) => getAuthor(comments[0].id))
  .then((author) => console.log(author))
  .catch(handleError); // single error handling

// The same with async/await — reads like synchronous code
async function load() {
  try {
    const user = await getUser(1);
    const posts = await getPosts(user.id);
    const comments = await getComments(posts[0].id);
    const author = await getAuthor(comments[0].id);
    console.log(author);
  } catch (err) {
    handleError(err);
  }
}

Problems with callback hell: hard to read, duplicated error handling, no single control flow, inversion of control (we trust someone else's code to call the callback correctly).

⚠️ Gotcha: The Node "error-first callback" convention (err, data) => {} — it is easy to forget to check err, and unhandled errors in callbacks are not caught by an outer try/catch (asynchrony).

24

24. Destructuring?

Short answer: Syntax for extracting values from arrays/objects into variables. Supports default values, renaming, nesting, and rest.

In detail:

// Arrays — by position
const [first, second, , fourth] = [1, 2, 3, 4]; // skip the third
console.log(first, second, fourth); // 1 2 4

// Swap values without a temp
let a = 1, b = 2;
[a, b] = [b, a]; // a=2, b=1

// Objects — by key name
const user = { name: "Anya", age: 30, city: "Moscow" };
const { name, age } = user;

// Renaming
const { name: userName } = user; // userName = "Anya"

// Default values (only for undefined!)
const { role = "user" } = user; // role = "user" (not in the object)

// Nested destructuring
const data = { user: { profile: { email: "a@b.com" } } };
const { user: { profile: { email } } } = data;
console.log(email); // "a@b.com"

// In function parameters
function greet({ name, greeting = "Hi" }) {
  return `${greeting}, ${name}`;
}
greet({ name: "Bob" }); // "Hi, Bob"

// With rest
const { name: n, ...rest } = user; // rest = { age, city }

⚠️ Gotcha: Destructuring assignment (without let/const) requires parentheses, otherwise { is parsed as a block:

let x;
({ x } = { x: 5 }); // parentheses are needed

Also, destructuring null/undefined throws: const { a } = nullTypeError.

25

25. Spread / rest?

Short answer: The same ... syntax. Spread — "unpacks" an iterable/object (in calls, in literals). Rest — "collects" the remainder into an array/object (in parameters, in destructuring).

In detail:

// SPREAD — unpacking
const arr = [1, 2, 3];
const copy = [...arr];             // shallow copy
const merged = [...arr, 4, 5];     // [1,2,3,4,5]
Math.max(...arr);                  // 3 — spread into arguments

const obj = { a: 1, b: 2 };
const objCopy = { ...obj, c: 3 };  // { a:1, b:2, c:3 }
const override = { ...obj, b: 99 };// { a:1, b:99 } — the last one wins

// String → array of characters
const chars = [..."abc"]; // ["a","b","c"]

// REST — collecting the remainder
function sum(...nums) {        // all arguments → array
  return nums.reduce((s, n) => s + n, 0);
}
sum(1, 2, 3, 4); // 10

const [head, ...tail] = [1, 2, 3]; // head=1, tail=[2,3]
const { a, ...others } = obj;      // others = { b: 2 }

// Rest must be last
function f(first, ...rest) {} // OK
// function f(...rest, last) {} // SyntaxError

⚠️ Gotcha: Spread makes only a SHALLOW copy — nested objects stay shared by reference:

const original = { nested: { x: 1 } };
const copy = { ...original };
copy.nested.x = 99;
console.log(original.nested.x); // 99 — the nested object was not copied!
26

26. Template literals?

Short answer: Strings in backticks with ${...} interpolation, multiline support, and tagged templates.

In detail:

const name = "Anya";
const age = 30;

// Interpolation
const greeting = `Hi, ${name}! You are ${age} years old.`;

// Expressions inside
const msg = `In a year it will be ${age + 1}`;
const cond = `Status: ${age >= 18 ? "adult" : "child"}`;

// Multiline without \n
const html = `
  <div>
    <h1>${name}</h1>
  </div>
`;

// Tagged templates — the function receives the parts
function highlight(strings, ...values) {
  return strings.reduce((acc, str, i) => {
    const val = values[i] ? `<b>${values[i]}</b>` : "";
    return acc + str + val;
  }, "");
}
const result = highlight`Name: ${name}, age: ${age}`;
// "Name: <b>Anya</b>, age: <b>30</b>"

Tagged templates are used for escaping (XSS protection), localization, and CSS-in-JS (styled-components).

⚠️ Gotcha: A multiline template preserves ALL indentation and line breaks as-is — code indentation ends up in the string. This affects string comparison and output.

27

27. map / filter / reduce / forEach / find / some / every?

Short answer: Array iteration methods. map — transformation, filter — selection, reduce — folding, forEach — side effects, find — the first match, some/every — checks.

In detail:

const nums = [1, 2, 3, 4, 5];

// map — new array of the same length (1:1 transformation)
nums.map((n) => n * 2);        // [2, 4, 6, 8, 10]

// filter — new array with selected elements
nums.filter((n) => n % 2 === 0); // [2, 4]

// reduce — fold to a single value
nums.reduce((acc, n) => acc + n, 0); // 15

// forEach — side effects only, returns undefined
nums.forEach((n) => console.log(n)); // returns nothing

// find — the first matching element (or undefined)
nums.find((n) => n > 3);       // 4
// findIndex — the index of the first match
nums.findIndex((n) => n > 3);  // 3

// some — is there at least one match → boolean
nums.some((n) => n > 4);       // true

// every — do all match → boolean
nums.every((n) => n > 0);      // true
Method Returns Stops
map new array no
filter new array no
reduce accumulator no
forEach undefined no
find element / undefined on the first true
some boolean on the first true
every boolean on the first false

Key point: map/filter/reduce do NOT mutate the source array, they return a new one. forEach cannot be interrupted with break/return (use for...of or some).

⚠️ Gotcha: Using map for side effects (without using the result) is an anti-pattern; forEach exists for that. And forEach will not wait for an await inside the callback — for asynchronous iteration use for...of.

28

28. reduce in detail?

Short answer: reduce(callback(accumulator, current, index, array), initialValue) reduces an array to a single value, running each element through the callback and accumulating the result in the accumulator.

In detail:

// Sum
[1, 2, 3, 4].reduce((acc, cur) => acc + cur, 0); // 10
// acc: 0→1→3→6→10

// Without an initial value — the first element becomes acc
[1, 2, 3].reduce((acc, cur) => acc + cur); // 6, acc starts at 1

// Maximum
[3, 7, 2].reduce((max, n) => (n > max ? n : max)); // 7

// Grouping by key
const people = [
  { name: "Anya", city: "Msk" },
  { name: "Bob", city: "Spb" },
  { name: "Vera", city: "Msk" },
];
people.reduce((groups, p) => {
  (groups[p.city] ||= []).push(p.name);
  return groups;
}, {});
// { Msk: ["Anya", "Vera"], Spb: ["Bob"] }

// Counting frequencies
["a", "b", "a", "c", "a"].reduce((acc, x) => {
  acc[x] = (acc[x] || 0) + 1;
  return acc;
}, {});
// { a: 3, b: 1, c: 1 }

// Function composition (pipe)
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const addThenDouble = pipe((n) => n + 1, (n) => n * 2);
addThenDouble(5); // 12

// "Flatten" nested arrays (flat by hand)
[[1, 2], [3, 4]].reduce((acc, arr) => acc.concat(arr), []); // [1,2,3,4]

⚠️ Gotcha: reduce WITHOUT an initial value on an empty array throws TypeError: Reduce of empty array with no initial value. Always provide an initialValue (especially when the accumulator type differs from the elements — an object, or a number over an array of objects).

29

29. Copying objects: shallow vs deep?

Short answer: Shallow copying copies the top level; nested objects stay by reference. Deep copying copies everything recursively. The modern way to deep copy is structuredClone.

In detail:

const original = {
  name: "Anya",
  address: { city: "Moscow" },
};

// SHALLOW — spread / Object.assign
const shallow1 = { ...original };
const shallow2 = Object.assign({}, original);
shallow1.address.city = "Spb";
console.log(original.address.city); // "Spb" — the nested object is shared!

// DEEP — structuredClone (modern, built-in)
const deep = structuredClone(original);
deep.address.city = "Kazan";
console.log(original.address.city); // unchanged

// DEEP — JSON hack (with caveats)
const jsonClone = JSON.parse(JSON.stringify(original));

Problems with the JSON hack:

const obj = {
  date: new Date(),       // → string
  fn: () => {},           // → lost
  undef: undefined,       // → lost
  sym: Symbol(),          // → lost
  inf: Infinity,          // → null
  nan: NaN,               // → null
  map: new Map(),         // → {} (empty object)
};
JSON.parse(JSON.stringify(obj));
// { date: "2026-...string", inf: null, nan: null, map: {} }
// circular references → TypeError

structuredClone supports Date, Map, Set, RegExp, ArrayBuffer, circular references, but NOT functions, DOM nodes, or prototypes (it loses the class) — it throws on functions.

Method Depth Functions Cycles Date/Map/Set
spread / assign shallow
JSON deep lost throws breaks
structuredClone deep throws OK OK

⚠️ Gotcha: structuredClone loses the prototype — the clone is a plain {} object, a class instance stops being an instance (instanceof → false), and methods are lost.

30

30. Mutability and pass by reference/value?

Short answer: Primitives are passed by value (a copy), objects by reference (a copy of the reference). Technically JS is always "pass by value", but for objects the value is the reference.

In detail:

// Primitive — copy of the value
function changePrimitive(x) {
  x = 100;
}
let a = 5;
changePrimitive(a);
console.log(a); // 5 — unchanged

// Object — copy of the REFERENCE (both point to the same object)
function mutateObject(obj) {
  obj.value = 100; // mutating the shared object
}
const o = { value: 5 };
mutateObject(o);
console.log(o.value); // 100 — changed!

// But reassigning the reference inside the function does NOT affect the outside
function reassign(obj) {
  obj = { value: 999 }; // new reference, local
}
const o2 = { value: 5 };
reassign(o2);
console.log(o2.value); // 5 — outside, the reference is unchanged

// That is why array methods split into mutating and non-mutating
const arr = [3, 1, 2];
arr.sort();        // MUTATES the original
arr.push(4);       // MUTATES
const sorted = [...arr].sort(); // safe — a copy
// Immutable: map, filter, slice, concat, [...spread]
// Mutating: push, pop, splice, sort, reverse, shift, unshift, fill

⚠️ Gotcha: sort() and reverse() mutate the array AND return the same array (one reference). const b = a.sort()a and b are the same array. The newer methods toSorted, toReversed, toSpliced, with return a copy.

31

31. Modules: ES Modules vs CommonJS?

Short answer: ESM (import/export) — the standard, static analysis, asynchronous loading, works in the browser and Node. CommonJS (require/module.exports) — the old Node format, synchronous, dynamic.

In detail:

// ===== ES Modules =====
// export
export const name = "Anya";           // named export
export function greet() {}            // named
export default class User {}          // default (one per module)

// import
import User from "./user.js";              // default
import { name, greet } from "./utils.js";  // named
import { name as userName } from "./u.js"; // renaming
import * as utils from "./utils.js";       // everything
import User, { name } from "./user.js";    // default + named

// Dynamic import — returns a promise
const module = await import("./heavy.js"); // lazy loading

// ===== CommonJS (Node) =====
const fs = require("fs");                   // import
const { readFile } = require("fs");         // destructuring
module.exports = { name, greet };           // export
module.exports.foo = 1;                     // or one at a time
exports.bar = 2;                            // a reference to module.exports
ESM CommonJS
Syntax import/export require/module.exports
Loading asynchronous, static synchronous, dynamic
Analysis static (tree-shaking) dynamic
top-level this undefined module.exports
When it resolves before execution (hoisted) at the moment of require

⚠️ Gotcha: In ESM import is hoisted and resolved statically — you cannot import inside an if (only import()). And ESM imports are "live" bindings (a change to the export is visible to the importer), whereas CommonJS require gives back a snapshot copy of the value at the time of the call.

32

32. IIFE?

Short answer: Immediately Invoked Function Expression — a function that is declared and called right away. It creates an isolated scope. Before ES6 it was the main way to encapsulate (the "module" pattern).

In detail:

// Basic syntax
(function () {
  const secret = "private";
  console.log(secret);
})(); // invoked immediately

// With an arrow function
(() => {
  console.log("executed immediately");
})();

// Passing arguments
(function (global) {
  // global = window, protection against redefinition
})(window);

// Module pattern — private state via closure
const counter = (function () {
  let count = 0; // private
  return {
    increment() { return ++count; },
    get() { return count; },
  };
})();
counter.increment(); // 1
console.log(counter.count); // undefined — not accessible

// Why the parentheses: to turn a declaration into an expression
// function(){}() — SyntaxError
// (function(){})() — OK

In modern code the role of the IIFE has been taken over by ES modules (their own scope) and block scoping with let/const. But IIFEs are still seen, including with async ((async () => { await ... })()).

⚠️ Gotcha: Without parentheses, function(){}() is parsed as a function declaration and throws an error. Alternatives to parentheses: !function(){}(), +function(){}() (use unary operators to get an expression).

33

33. debounce and throttle?

Short answer: Both limit how often a function is called. Debounce delays the call until a pause (it runs after you stop triggering it). Throttle guarantees the call happens no more than once per interval.

In detail:

// DEBOUNCE — call after delay following the LAST event
function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
// Use case: search-as-you-type (wait until the user finishes typing)
const onSearch = debounce((q) => console.log("Search:", q), 300);

// THROTTLE — call no more than once per limit ms
function throttle(fn, limit) {
  let inThrottle = false;
  return function (...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}
// Use case: scroll, resize, mousemove (evenly, not every frame)
const onScroll = throttle(() => console.log("scroll"), 200);

The difference on a timeline (events come densely):

Events:   | | | | | | | | |
Debounce:                    X   (once, after the pause)
Throttle: X     X     X     X    (evenly, once per interval)

Use cases:

  • debounce: autocomplete/search, form validation, auto-save.
  • throttle: handling scroll/resize/mousemove, limiting API requests.

⚠️ Gotcha: In implementations it is important to preserve this and args via fn.apply(this, args), otherwise the event handler will lose its context and the event data. Also, debounce often needs an immediate flag (fire right away on the first event) and a cancel method.

34

34. Symbol?

Short answer: Symbol is a primitive type that creates unique, non-enumerable identifiers. It is used for "hidden" object properties and system hooks (well-known symbols).

In detail:

// Each symbol is unique
const s1 = Symbol("desc");
const s2 = Symbol("desc");
console.log(s1 === s2); // false — even with the same description

// As an object key — does not clash with regular keys
const id = Symbol("id");
const user = { name: "Anya", [id]: 123 };
console.log(user[id]); // 123
console.log(Object.keys(user)); // ["name"] — the symbol is not visible
for (const k in user) console.log(k); // only "name"

// Global symbol registry
const g1 = Symbol.for("app.id"); // creates or retrieves an existing one
const g2 = Symbol.for("app.id");
console.log(g1 === g2); // true

// Well-known symbols — customizing behavior
class Range {
  constructor(start, end) { this.start = start; this.end = end; }
  [Symbol.iterator]() {     // makes the object iterable
    let cur = this.start;
    const end = this.end;
    return {
      next() {
        return cur <= end
          ? { value: cur++, done: false }
          : { value: undefined, done: true };
      },
    };
  }
}
console.log([...new Range(1, 3)]); // [1, 2, 3]

Symbols do NOT show up in Object.keys, for...in, or JSON.stringify. You can get them via Object.getOwnPropertySymbols.

⚠️ Gotcha: Symbols from Symbol() and Symbol.for() are DIFFERENT: the first is always unique, the second is taken from the global registry. Also, a symbol cannot be implicitly coerced to a string ("" + Symbol() → TypeError), only via .toString() or String(sym).

35

35. Iterators and generators?

Short answer: An iterator is an object with a next() method that returns { value, done }. A generator (function*) is a function that can be paused via yield and automatically implements the iterator protocol.

In detail:

// Generator — function* and yield
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}
const it = gen();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }

// A generator is iterable
for (const v of gen()) console.log(v); // 1 2 3
console.log([...gen()]);                // [1, 2, 3]

// Infinite sequence (lazy)
function* naturals() {
  let n = 1;
  while (true) yield n++;
}
const nums = naturals();
nums.next().value; // 1
nums.next().value; // 2 — generated on demand

// Two-way communication — yield returns the value from next(arg)
function* dialog() {
  const name = yield "What is your name?";
  yield `Hi, ${name}!`;
}
const d = dialog();
d.next();           // { value: "What is your name?", done: false }
d.next("Anya");     // { value: "Hi, Anya!", done: false }

// Delegation with yield*
function* combined() {
  yield* [1, 2];
  yield* gen();
}

Use cases for generators: lazy/infinite sequences, custom iteration, coroutines, libraries (redux-saga).

⚠️ Gotcha: A generator object is single-use — after done: true you cannot "rewind" it; you need to create a new one by calling gen(). And the first next() runs the code only up to the first yield; the argument to the very first next() is ignored.

36

36. Optional chaining (?.) and nullish coalescing (??)?

Short answer: ?. accesses a property safely — it returns undefined instead of throwing if the left side is null/undefined. ?? returns the right operand only if the left is null/undefined (unlike ||).

In detail:

// Optional chaining ?.
const user = { profile: { name: "Anya" } };
console.log(user.profile?.name);       // "Anya"
console.log(user.settings?.theme);     // undefined (not an error!)
console.log(user.address?.city);       // undefined

// Without ?. it would be an error
// user.address.city → TypeError: Cannot read properties of undefined

// With method calls and arrays
user.getName?.();          // calls it if the method exists, otherwise undefined
user.list?.[0];            // safe access by index

// Nullish coalescing ??
const a = 0 ?? 10;         // 0  (0 is not nullish)
const b = null ?? 10;      // 10
const c = undefined ?? 10; // 10
const d = "" ?? "default"; // ""  (an empty string is not nullish)

// The difference from ||
const count1 = 0 || 5;  // 5  (|| treats 0 as falsy → replaces it)
const count2 = 0 ?? 5;  // 0  (?? replaces only null/undefined)

// Combination
const theme = user.settings?.theme ?? "light"; // "light"

|| triggers on all falsy values (0, "", false, NaN), while ?? triggers only on null/undefined. That is why ?? is more correct for default values where 0 and "" are valid.

⚠️ Gotcha: You cannot mix ?? with ||/&& without parentheses — it is a syntax error: a ?? b || c → SyntaxError. You need (a ?? b) || c. Also, ?. only "short-circuits" the access, but does not protect against an error in the next expression outside the chain.

37

37. Garbage collection and memory leaks?

Short answer: JS manages memory automatically through a garbage collector using the mark-and-sweep algorithm: objects unreachable from the roots are removed. Leaks happen when unneeded objects remain reachable.

In detail:

The mark-and-sweep algorithm:

  1. The collector starts from the roots (global object, the current call stack).
  2. Mark: it marks all objects reachable by references from the roots.
  3. Sweep: it frees the memory of unmarked (unreachable) objects.

Reachability, not reference counting — that is why circular references do not cause leaks (if the whole cycle is unreachable from the roots).

let obj = { data: "a big object" };
obj = null; // the object became unreachable → it will be collected

// Common leaks:

// 1. Forgotten timers/intervals
const id = setInterval(() => { /* holds a closure */ }, 1000);
// you need clearInterval(id) when it is no longer needed

// 2. Forgotten event handlers
element.addEventListener("click", handler);
// holds element and handler; you need removeEventListener

// 3. Closures holding large objects
function leak() {
  const huge = new Array(1000000);
  return () => huge[0]; // the closure holds the entire huge
}

// 4. Accidental global variables (without declaration, non-strict)
function bad() { leaked = "global"; } // window.leaked lives forever

// 5. Growing caches/Maps without cleanup — use WeakMap
const cache = new WeakMap(); // object keys are collected automatically

WeakMap/WeakSet hold "weak" references — they do not prevent collection of object keys, which avoids leaks in caches.

⚠️ Gotcha: Detached DOM nodes — a node removed from the DOM but still referenced by a JS variable or a handler closure → it is not collected. Also, console.log(obj) in DevTools can keep a reference. Profile with a Memory snapshot in DevTools.

38

38. Object: keys/values/entries, freeze, getters/setters?

Short answer: Object.keys/values/entries extract keys/values/pairs. Object.freeze makes an object immutable (shallowly). Getters/setters are methods that look like properties.

In detail:

const user = { name: "Anya", age: 30 };

Object.keys(user);    // ["name", "age"]
Object.values(user);  // ["Anya", 30]
Object.entries(user); // [["name","Anya"], ["age",30]]

// entries is handy for iteration and transformations
for (const [key, value] of Object.entries(user)) {
  console.log(`${key}: ${value}`);
}
// An object back from pairs
Object.fromEntries([["a", 1], ["b", 2]]); // { a: 1, b: 2 }

// freeze — disallow changes (shallow!)
const frozen = Object.freeze({ x: 1, nested: { y: 2 } });
frozen.x = 99;          // silently ignored (TypeError in strict)
frozen.nested.y = 99;   // WILL CHANGE — freeze is not recursive
console.log(Object.isFrozen(frozen)); // true

// Getters / Setters
const temp = {
  _celsius: 0,
  get celsius() { return this._celsius; },
  set celsius(val) { this._celsius = val; },
  get fahrenheit() { return this._celsius * 1.8 + 32; },
  set fahrenheit(val) { this._celsius = (val - 32) / 1.8; },
};
temp.celsius = 25;
console.log(temp.fahrenheit); // 77 — computed on access
temp.fahrenheit = 212;
console.log(temp.celsius);    // 100

⚠️ Gotcha: Object.freeze is shallow — nested objects stay mutable. For a deep freeze you need recursion (deepFreeze). Object.keys does NOT traverse inherited or symbol properties (unlike for...in, which traverses inherited enumerable ones).

39

39. Map / Set vs object / array?

Short answer: Map is a collection of key-value pairs with keys of any type and preserved order. Set is a collection of unique values. They are better than an object/array for frequent additions/removals and lookups.

In detail:

// MAP — keys of any type, has .size, preserves order
const map = new Map();
map.set("str", 1);
map.set(42, 2);
const objKey = {};
map.set(objKey, 3);     // an object as a key!
map.get(objKey);        // 3
map.has("str");         // true
map.size;               // 3
map.delete(42);
for (const [k, v] of map) console.log(k, v); // iterable

// SET — unique values
const set = new Set([1, 2, 2, 3, 3]);
console.log([...set]);  // [1, 2, 3] — duplicates removed
set.add(4);
set.has(2);             // true
set.size;               // 4
// Deduplicating an array
const unique = [...new Set([1, 1, 2, 3])]; // [1, 2, 3]
Map Object
Keys any type string / symbol
Size .size Object.keys().length
Order guaranteed mostly yes
Iteration directly (for...of) via Object.entries
Insert/delete performance optimized worse with frequent changes
Prototype no "garbage" keys has one (risk of collisions)

When to use which: Map — a dynamic dictionary with arbitrary keys; object — a fixed structure/record; Set — uniqueness/membership checks; array — an ordered list with indices.

⚠️ Gotcha: A Map cannot be serialized via JSON.stringify (it gives {}). And Set/Map compare object keys by reference, not by content: set.add({a:1}); set.has({a:1})false.

40

40. Strict mode?

Short answer: "use strict" enables strict mode: it forbids unsafe constructs, turns silent errors into exceptions, and changes this in ordinary calls to undefined. In ES modules and classes it is enabled automatically.

In detail:

"use strict"; // at the start of a file or function

// 1. You cannot create undeclared global variables
function f() {
  undeclared = 5; // ReferenceError (without strict it would create window.undeclared)
}

// 2. this in an ordinary call = undefined (not window)
function g() {
  console.log(this); // undefined (without strict — window)
}
g();

// 3. Error when writing to a non-writable property
const obj = Object.freeze({ x: 1 });
obj.x = 2; // TypeError (without strict — silently ignored)

// 4. Duplicate parameters are forbidden
// function h(a, a) {} // SyntaxError

// 5. Octal literals 0123, with, deleting a variable are forbidden

Where strict is enabled automatically: the body of a class, ES modules (import/export), so modern code is strict by default.

⚠️ Gotcha: "use strict" must be the FIRST statement in the file/function — otherwise it is silently ignored. A function-level directive makes only that function strict. In modules you do not need to write it.

41

41. Why do we need closures?

Short answer: Closures provide encapsulation (private state), data persistence between calls, function factories and currying, and they underpin callbacks, handlers, and modules.

In detail:

// 1. Encapsulation of private state (no access from outside)
function createStore(initial) {
  let state = initial;
  return {
    getState: () => state,
    setState: (next) => { state = next; },
  };
}

// 2. Memory between calls (memoization)
function memoize(fn) {
  const cache = new Map(); // the closure holds the cache
  return (arg) => {
    if (cache.has(arg)) return cache.get(arg);
    const result = fn(arg);
    cache.set(arg, result);
    return result;
  };
}

// 3. Currying / partial application
const add = (a) => (b) => a + b; // b closes over a
const add5 = add(5);
add5(3); // 8

// 4. Capturing configuration (factories)
const createLogger = (prefix) => (msg) => console.log(`[${prefix}] ${msg}`);
const errorLog = createLogger("ERROR");

In essence almost all functional JS, React hooks (useState keeps state in a closure), event handlers, and debounce/throttle are all built on closures.

⚠️ Gotcha: Closures keep the entire lexical environment in memory. If a closure references even one variable from a large scope, the engine may hold onto it → a potential leak.

42

42. Why is JS single-threaded but non-blocking?

Short answer: JS runs code in a single thread (one call stack), but long-running operations (I/O, timers, network) are delegated to the environment (browser/Node), which executes them asynchronously and pushes the callbacks onto a queue. The event loop picks them up when the stack is free.

In detail:

// Synchronous blocking code would freeze everything
// while (true) {} // would hang the tab — there's only one thread

// Asynchrony: the operation goes off to the Web API, the thread is free
console.log("1");
setTimeout(() => console.log("3 — later"), 0); // goes off to the browser
fetch("/data").then(() => console.log("data")); // network in the background
console.log("2");
// Output: 1, 2, 3 — the thread didn't wait for the timer

// Heavy computation still blocks (no I/O delegate)
function heavy() {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) sum += i; // will freeze the UI
  return sum;
}
// The solution for CPU-bound tasks is Web Workers (a separate thread)

The key point: the JS engine itself is single-threaded, but the environment (browser, Node/libuv) is multi-threaded and takes on the I/O. The JS thread doesn't wait — it registers a callback and continues. When the operation finishes, the callback lands in the queue, and the event loop runs it once the stack is free.

⚠️ Gotcha: "Non-blocking" applies only to asynchronous I/O operations. Heavy synchronous computation (large loops, complex regexes, JSON.parse of a giant object) blocks the single thread and freezes the UI. For those you need Web Workers.

43

43. What's wrong with this in JS?

Short answer: this is determined at the moment of the CALL, not at declaration, and depends on how the function is called — this is counterintuitive and leads to the common "loss of context" problem. Arrow functions (with their lexical this) largely solve it.

In detail:

const obj = {
  name: "Anya",
  greet() {
    console.log(this.name); // "Anya" when called as obj.greet()
  },
};

// 1. Losing this when the method is "detached"
const fn = obj.greet;
fn(); // undefined/error — this is no longer obj

// 2. this is lost inside a callback
[1].forEach(function () {
  // this here is not obj, but undefined/window
});

// 3. this inside a nested ordinary function
const obj2 = {
  items: [1, 2],
  process() {
    this.items.forEach(function () {
      // this.items is unavailable here — a different this
    });
  },
};

// Solution — arrow functions (lexical this)
const obj3 = {
  items: [1, 2],
  process() {
    this.items.forEach(() => console.log(this.items)); // works
  },
};

The problem is that this is dynamic and depends on 4 call rules. In most languages this (or self) refers to the instance reliably. In JS the same method gives a different this depending on how it was called.

⚠️ Gotcha: Don't use arrow functions where this MUST be dynamic: object/prototype methods, DOM handlers (where this = element is expected). An arrow function is "hard"-lexical, and call/apply/bind on it won't override this.

44

44. Why use Promises if we already have callbacks?

Short answer: Promises solve the problems of callbacks: callback hell, duplicated error handling, inversion of control. They give you flat chains, a single catch, composition (all/race), and the foundation for async/await.

In detail:

// The callback problem: nesting + error handling at every level
loadA((errA, a) => {
  if (errA) return handle(errA);
  loadB(a, (errB, b) => {
    if (errB) return handle(errB); // duplicated handling
    // ...the pyramid keeps growing
  });
});

// Promises: flat, a single catch, guarantees
loadA()
  .then((a) => loadB(a))
  .then((b) => loadC(b))
  .catch(handle); // one error-handling point

// Composition — not achievable elegantly with callbacks
const [a, b] = await Promise.all([loadA(), loadB()]);

Additional guarantees of promises:

  • The then callback is called exactly once (a plain callback might be called 0, 1, or many times — a bug in someone else's code).
  • The callback is always asynchronous (even on an already-resolved promise), which prevents race conditions.
  • They solve inversion of control: with a callback you trust someone else's code to call yours correctly; a promise is a promise-object that you control.

⚠️ Gotcha: Promises are not cancellable (Promise.all won't stop the others on an error), and if you forget .catch, a rejected promise triggers unhandledrejection. Don't wrap APIs that already return a promise by hand in new Promise (the "promise constructor" anti-pattern).

45

45. Why is 0.1 + 0.2 !== 0.3?

Short answer: Numbers in JS are doubles (IEEE 754, 64 bits). The fractions 0.1 and 0.2 have no exact binary representation (like 1/3 in decimal), so rounding error accumulates.

In detail:

console.log(0.1 + 0.2);            // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3);   // false

// Why: 0.1 in binary is an infinite fraction
// 0.1₁₀ = 0.0001100110011...₂ (repeating)
// Stored with rounding in 64 bits → a tiny error

// Correct comparison — via an epsilon
function nearlyEqual(a, b, eps = Number.EPSILON) {
  return Math.abs(a - b) < eps;
}
nearlyEqual(0.1 + 0.2, 0.3); // true

// Rounding to the required precision
(0.1 + 0.2).toFixed(2);          // "0.30" (a string)
Math.round((0.1 + 0.2) * 100) / 100; // 0.3

// For money — store in cents (integers) or BigInt
const cents = 10 + 20; // 30 cents, no errors

Number.EPSILON is the smallest difference between 1 and the next representable number (~2.22e-16). It's used as a threshold for floating-point comparison.

⚠️ Gotcha: This is NOT a JS bug — that's how IEEE 754 works in every language (Python, Java, C). Never compare fractional numbers with ===; for money use integers (minimal units) or special libraries (decimal.js). 0.1 + 0.2 produces an extra error, while 0.5 + 0.5 === 1 is true (powers of two are represented exactly).

Source notes

References and review policy

RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going

RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS