JavaScript drives nearly every web frontend and an increasing share of backend (Node.js). TypeScript has become the default for serious JS projects. This guide covers the most common JS/TS interview questions with working code examples.
Core JavaScript Concepts
1. How does the JavaScript Event Loop work?
/**
* JavaScript is single-threaded — one call stack.
* The Event Loop coordinates async work:
*
* Call Stack
* ┌──────────────┐
* │ setTimeout │ → Web API/Node timer → Callback Queue
* │ fetch() │ → Web API/libuv → Microtask Queue (Promise)
* └──────────────┘
*
* Priority: Call Stack → Microtask Queue (Promises) → Macrotask Queue (setTimeout, I/O)
*
* Microtasks run after EVERY task, draining completely before next macrotask.
*/
console.log("1 - sync");
setTimeout(() => console.log("4 - setTimeout (macrotask)"), 0);
Promise.resolve()
.then(() => console.log("2 - promise microtask"))
.then(() => console.log("3 - chained microtask"));
console.log("1.5 - still sync");
// Output order: 1, 1.5, 2, 3, 4
// Explanation: sync runs first, then microtasks flush completely, then macrotask
2. Closures — how and why they work
// A closure is a function that retains access to its lexical scope
// even after the outer function has returned.
function makeCounter(initial = 0) {
let count = initial; // Closed-over variable
return {
increment: () => ++count,
decrement: () => --count,
reset: () => { count = initial; },
value: () => count,
};
}
const c = makeCounter(10);
c.increment(); // 11
c.increment(); // 12
c.decrement(); // 11
c.reset(); // 10
// Classic bug: closure in loops
const badFns = [];
for (var i = 0; i i); // All capture same `i` (var is function-scoped)
}
console.log(badFns.map(f => f())); // [3, 3, 3] — wrong!
// Fix 1: use let (block-scoped — creates new binding each iteration)
const goodFns = [];
for (let i = 0; i i);
}
console.log(goodFns.map(f => f())); // [0, 1, 2]
// Fix 2: IIFE to capture value
const iifeFns = [];
for (var i = 0; i () => n)(i));
}
console.log(iifeFns.map(f => f())); // [0, 1, 2]
// Practical use: memoization
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (!cache.has(key)) {
cache.set(key, fn.apply(this, args));
}
return cache.get(key);
};
}
3. Prototypal Inheritance and the `this` keyword
// JavaScript uses prototype chain for inheritance
// Every object has [[Prototype]] (accessed via __proto__ or Object.getPrototypeOf)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a noise.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
return `${this.name} barks.`;
}
}
// Under the hood, classes are syntactic sugar over prototypes:
// Dog.prototype.__proto__ === Animal.prototype
// new Dog("Rex").speak() → looks up Dog.prototype.speak first
// `this` binding rules (in priority order):
// 1. new binding: new Foo() → `this` = new object
// 2. Explicit binding: foo.call(ctx) → `this` = ctx
// 3. Implicit binding: obj.foo() → `this` = obj
// 4. Default binding: foo() → `this` = global (undefined in strict mode)
// Arrow functions: no own `this` → inherits from lexical scope
const obj = {
name: "Alice",
greet: function() { return `Hi, ${this.name}`; },
greetArrow: () => `Hi, ${this?.name}`, // `this` is outer scope (likely global)
};
const greet = obj.greet;
greet(); // "Hi, undefined" — lost binding (default binding)
obj.greet(); // "Hi, Alice" — implicit binding
greet.call(obj); // "Hi, Alice" — explicit binding
greet.bind(obj)(); // "Hi, Alice" — bound function
4. Promises and async/await
// Promise states: pending → fulfilled | rejected (terminal states)
// Sequential vs parallel async:
async function sequential() {
const a = await fetch("/api/a"); // Wait for a
const b = await fetch("/api/b"); // Then wait for b
// Total time: latency_a + latency_b
}
async function parallel() {
const [a, b] = await Promise.all([fetch("/api/a"), fetch("/api/b")]);
// Total time: max(latency_a, latency_b)
}
// Promise combinators:
async function robustFetch(urls) {
// allSettled: never rejects — gets results OR errors for all
const results = await Promise.allSettled(urls.map(u => fetch(u)));
return results.map(r => r.status === "fulfilled" ? r.value : null);
}
async function race(urls) {
// race: first to resolve/reject wins
return Promise.race(urls.map(u => fetch(u)));
}
async function firstSuccess(urls) {
// any: first to FULFILL (ignores rejections unless all fail)
return Promise.any(urls.map(u => fetch(u)));
}
// Retry with exponential backoff:
async function withRetry(fn, maxAttempts = 3, baseDelayMs = 500) {
for (let attempt = 0; attempt setTimeout(r, baseDelayMs * 2 ** attempt));
}
}
}
5. TypeScript Generics and Advanced Types
// Generics enable type-safe reusable code
function identity(arg: T): T { return arg; }
const num = identity(42); // T inferred as number
const str = identity("hi"); // T inferred as string
// Constrained generics
function getProperty(obj: T, key: K): T[K] {
return obj[key];
}
const name = getProperty({name: "Alice", age: 30}, "name"); // string
// Utility types
interface User {
id: number;
name: string;
email: string;
createdAt: Date;
}
type UpdateUserDTO = Partial<Pick>; // optional subset
type PublicUser = Omit; // exclude field
type ReadonlyUser = Readonly; // all readonly
type UserRecord = Record; // index signature
// Conditional types
type NonNullable = T extends null | undefined ? never : T;
type Awaited = T extends Promise ? R : T;
type ReturnType = T extends (...args: any[]) => infer R ? R : never;
// Discriminated unions — exhaustive type narrowing
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rectangle"; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
case "rectangle": return shape.width * shape.height;
default:
// TypeScript exhaustiveness check:
const _exhaustive: never = shape;
throw new Error(`Unknown shape: ${_exhaustive}`);
}
}
6. Common Array/Object Patterns
// Immutable data patterns (React/Redux best practices)
const state = { user: { name: "Alice" }, items: [1, 2, 3] };
// Shallow copy — works for flat updates
const nextState = { ...state, items: [...state.items, 4] };
// Deep merge — structuredClone (modern, built-in)
const deep = structuredClone(state);
deep.user.name = "Bob"; // Does not mutate original
// Array transformations
const products = [
{ id: 1, category: "a", price: 10 },
{ id: 2, category: "b", price: 20 },
{ id: 3, category: "a", price: 15 },
];
// Group by category
const grouped = products.reduce((acc, p) => {
(acc[p.category] ??= []).push(p);
return acc;
}, {} as Record);
// Sort by price desc, then id asc
const sorted = [...products].sort((a, b) =>
b.price - a.price || a.id - b.id
);
// Pipe / compose
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const double = x => x * 2;
const addTen = x => x + 10;
const process = pipe(double, addTen);
console.log(process(5)); // (5*2)+10 = 20
Runtime Complexity of Built-in Operations
| Operation | Complexity | Notes |
|---|---|---|
| Array push/pop | O(1) amortized | Dynamic array |
| Array shift/unshift | O(n) | Shifts all elements |
| Array indexOf/find | O(n) | Linear scan |
| Map/Set get/has/set | O(1) average | Hash table |
| Object property access | O(1) average | V8 hidden classes optimize |
| Spread/Array.from | O(n) | Copies elements |
| JSON.stringify/parse | O(n) | n = total node count |
Frequently Asked Questions
How does the JavaScript event loop work?
JavaScript is single-threaded. The event loop coordinates asynchronous work: the call stack runs synchronous code, microtasks (Promise callbacks) are processed after each task draining completely, then macrotasks (setTimeout, I/O callbacks) are processed one per loop iteration. Microtasks always run before the next macrotask.
What is a closure in JavaScript?
A closure is a function that retains access to variables from its outer (enclosing) scope even after the outer function has returned. Closures are created every time a function is created in JavaScript. They enable patterns like module pattern (private variables), factory functions, memoization, and partial application.
What is the difference between == and === in JavaScript?
=== (strict equality) compares both value and type — no coercion. == (loose equality) coerces types before comparing, leading to surprising results like null == undefined being true, or 0 == "" being true. Always use === unless you specifically want type coercion, which is rarely the right choice.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does the JavaScript event loop work?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “JavaScript is single-threaded. The event loop coordinates asynchronous work: the call stack runs synchronous code, microtasks (Promise callbacks) are processed after each task draining completely, then macrotasks (setTimeout, I/O callbacks) are processed one per loop iteration. Microtasks always run before the next macrotask.”
}
},
{
“@type”: “Question”,
“name”: “What is a closure in JavaScript?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A closure is a function that retains access to variables from its outer (enclosing) scope even after the outer function has returned. Closures are created every time a function is created in JavaScript. They enable patterns like module pattern (private variables), factory functions, memoization, and partial application.”
}
},
{
“@type”: “Question”,
“name”: “What is the difference between == and === in JavaScript?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “=== (strict equality) compares both value and type — no coercion. == (loose equality) coerces types before comparing, leading to surprising results like null == undefined being true, or 0 == “” being true. Always use === unless you specifically want type coercion, which is rarely the right choice.”
}
}
]
}