JavaScript Interview Questions with Answers
Most Asked JavaScript Interview Questions for Software Engineer Roles
Introduction
This page provides a complete collection of JavaScript Interview Questions and Answers designed for frontend developers, full-stack engineers, and JavaScript enthusiasts preparing for technical interviews. JavaScript is the most popular programming language in the world, powering both client-side and server-side applications. Its event-driven, non-blocking nature makes it essential for modern web development. This guide covers beginner, intermediate, and advanced concepts including variables, functions, DOM manipulation, asynchronous programming, closures, ES6+ features, design patterns, and real-world coding problems.
Why JavaScript?
- Runs everywhere – browsers, servers (Node.js), and mobile
- Huge ecosystem with countless libraries and frameworks
- Asynchronous programming with promises and async/await
- Functional and object-oriented paradigms
- Essential for modern web development – a must-have skill for interviews
Most Asked JavaScript Interview Questions
JavaScript is a high-level, interpreted scripting language used to create dynamic and interactive web pages. It is one of the core technologies of the World Wide Web.
- Client-side: Runs in the browser
- Server-side: Runs on Node.js
- Event-driven: Responds to user actions
- Prototype-based: Uses prototypal inheritance
- Functional: Supports functional programming
// Hello World in JavaScript
console.log("Hello, World!");JavaScript is an interpreted language, meaning it is executed line by line by the browser's JavaScript engine. However, modern JavaScript engines use Just-In-Time (JIT) compilation for performance optimization.
- Interpreted: Executed at runtime without compilation
- JIT Compilation: Modern engines compile hot code
- Runtime: Code runs in the browser or Node.js
- Dynamic: Types are determined at runtime
// Variables in JavaScript
var old = "function scoped";
let block = "block scoped";
const constant = "cannot be reassigned";
console.log(old);
console.log(block);
console.log(constant);Variables are containers for storing data values. JavaScript provides three ways to declare variables: var, let, and const.
- var: Function-scoped, can be redeclared
- let: Block-scoped, can be reassigned
- const: Block-scoped, cannot be reassigned
- Naming Rules: Must start with letter, _, or $
// Data Types in JavaScript
let str = "Hello";
let num = 42;
let bool = true;
let nul = null;
let undef = undefined;
let sym = Symbol("id");
let big = 9007199254740991n;
let obj = { name: "Alice", age: 25 };
let arr = [1, 2, 3];
console.log(typeof str); // string
console.log(typeof num); // number
console.log(typeof bool); // boolean
console.log(typeof nul); // object (historical bug)
console.log(typeof undef); // undefined
console.log(typeof sym); // symbol
console.log(typeof big); // bigint
console.log(typeof obj); // object
console.log(typeof arr); // objectThe main differences between var, let, and const are scope, hoisting, and reassignment rules.
- var: Function-scoped, hoisted, can be redeclared
- let: Block-scoped, hoisted but not initialized (TDZ)
- const: Block-scoped, cannot be reassigned
- Best Practice: Use
constby default,letwhen reassignment needed
// Functions in JavaScript
// Function declaration
function add(a, b) {
return a + b;
}
// Function expression
const subtract = function(a, b) {
return a - b;
};
// Arrow function
const multiply = (a, b) => a * b;
// Default parameters
function greet(name = "Guest") {
return `Hello, ${name}!`;
}
// Rest parameters
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(add(5, 3));
console.log(subtract(10, 4));
console.log(multiply(6, 7));
console.log(greet("Alice"));
console.log(sum(1, 2, 3, 4, 5));JavaScript has 8 data types: 7 primitive types and 1 object type. Types are dynamic and can change at runtime.
- Primitive Types: String, Number, Boolean, Null, Undefined, Symbol, BigInt
- Reference Type: Object (arrays, functions, dates, etc.)
- typeof: Operator to check data type
- Dynamic Typing: Variables can hold any type
// Arrays in JavaScript
const arr = [1, 2, 3, 4, 5];
// Map - transform each element
const doubled = arr.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// Filter - select elements
const evens = arr.filter(x => x % 2 === 0);
console.log(evens); // [2, 4]
// Reduce - aggregate
const sum = arr.reduce((acc, x) => acc + x, 0);
console.log(sum); // 15
// forEach - iterate
arr.forEach(x => console.log(x));
// Find - find first match
const found = arr.find(x => x > 3);
console.log(found); // 4
// Some - check if any match
const hasEven = arr.some(x => x % 2 === 0);
console.log(hasEven); // true
// Every - check if all match
const allEven = arr.every(x => x % 2 === 0);
console.log(allEven); // falseA function is a reusable block of code designed to perform a specific task. Functions can take parameters and return values.
- Function Declaration:
function name() {} - Function Expression:
const func = function() {} - Arrow Function:
() => {} - Parameters: Values passed to the function
- Return: Value returned from the function
// Objects in JavaScript
// Object literal
const person = {
name: "Alice",
age: 25,
city: "NYC",
greet() {
return `Hello, I'm ${this.name}`;
}
};
console.log(person.name);
console.log(person.age);
console.log(person.greet());
// Object destructuring
const { name, age } = person;
console.log(name, age);
// Spread operator
const personCopy = { ...person, age: 26 };
console.log(personCopy);
// Object.keys, values, entries
console.log(Object.keys(person));
console.log(Object.values(person));
console.log(Object.entries(person));
// Object.assign
const merged = Object.assign({}, person, { country: "USA" });
console.log(merged);An array is a special object that stores multiple values in a single variable. Arrays are zero-indexed and can hold mixed data types.
- Creation:
[]ornew Array() - Access:
arr[index] - Methods: push, pop, shift, unshift, map, filter, reduce
- Properties: length, prototype
// DOM Manipulation
// Select elements
const element = document.getElementById("myId");
const elements = document.getElementsByClassName("myClass");
const query = document.querySelector(".myClass");
const all = document.querySelectorAll("div");
// Create elements
const div = document.createElement("div");
div.textContent = "Hello";
div.className = "my-class";
div.id = "my-id";
document.body.appendChild(div);
// Event listeners
button.addEventListener("click", function(e) {
console.log("Clicked!", e.target);
});
// Remove element
element.remove();
// Get/Set attributes
const value = element.getAttribute("data-value");
element.setAttribute("data-value", "new");
// Class manipulation
element.classList.add("active");
element.classList.remove("active");
element.classList.toggle("active");
element.classList.contains("active");
// InnerHTML vs textContent
element.innerHTML = "<span>HTML</span>";
element.textContent = "Plain text";An object is a collection of key-value pairs. Objects can contain properties and methods, and can be created using object literals or constructors.
- Creation:
ornew Object() - Properties: Key-value pairs
- Methods: Functions stored as properties
- Access:
obj.keyorobj["key"]
// Events in JavaScript
// Click event
button.addEventListener("click", function(event) {
console.log("Button clicked!", event);
});
// Mouse events
element.addEventListener("mouseenter", () => console.log("Mouse entered"));
element.addEventListener("mouseleave", () => console.log("Mouse left"));
element.addEventListener("mousemove", (e) => console.log(e.clientX, e.clientY));
// Keyboard events
document.addEventListener("keydown", (e) => {
console.log(`Key ${e.key} pressed`);
});
// Form events
form.addEventListener("submit", (e) => {
e.preventDefault();
console.log("Form submitted");
});
input.addEventListener("change", (e) => {
console.log("Value changed:", e.target.value);
});
input.addEventListener("input", (e) => {
console.log("Input:", e.target.value);
});
// Event delegation
document.addEventListener("click", (e) => {
if (e.target.matches(".button-class")) {
console.log("Button clicked via delegation");
}
});
// Remove event listener
button.removeEventListener("click", handler);DOM (Document Object Model) is a programming interface for HTML documents. It represents the page structure as a tree of objects that can be manipulated with JavaScript.
- Tree Structure: Document -> Elements -> Attributes
- Node Types: Element, Text, Comment, Document
- Methods: getElementById, querySelector, createElement
- Properties: innerHTML, textContent, style
// Hoisting in JavaScript
// Function hoisting
console.log(add(2, 3)); // Works: 5
function add(a, b) {
return a + b;
}
// Variable hoisting (var)
console.log(x); // undefined (not error)
var x = 10;
console.log(x); // 10
// Variable hoisting (let/const) - Temporal Dead Zone
// console.log(y); // ReferenceError
let y = 20;
// Function expression hoisting
// console.log(multiply(2, 3)); // TypeError
var multiply = function(a, b) {
return a * b;
};
// Class hoisting
// const obj = new MyClass(); // ReferenceError
class MyClass {
constructor() {
this.name = "test";
}
}An event is an action that occurs in the browser, such as user interaction or system events. JavaScript can respond to events using event listeners.
- Mouse Events: click, hover, mousemove
- Keyboard Events: keydown, keyup, keypress
- Form Events: submit, change, input
- Window Events: load, resize, scroll
// Closures in JavaScript
// Basic closure
function outer() {
let count = 0;
return function inner() {
count++;
return count;
};
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
// Closure with parameters
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplier(2);
console.log(double(5)); // 10
// Closure in loops
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 3, 3, 3
}
// Fix with let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}
// Private variables
function createCounter() {
let count = 0;
return {
increment() { count++; },
decrement() { count--; },
getValue() { return count; }
};
}
const counter2 = createCounter();
counter2.increment();
counter2.increment();
console.log(counter2.getValue()); // 2null represents an intentional absence of value. It is a primitive value that indicates "no value" or "empty".
- Type: typeof null returns "object" (historical bug)
- Assignment: Can be assigned to variables
- Comparison: null == undefined (true), null === undefined (false)
- Use Case: Indicates missing object reference
// Scope in JavaScript
// Global scope
const globalVar = "global";
function testScope() {
// Function scope
var functionScoped = "function";
// Block scope
if (true) {
let blockScoped = "block";
const blockConst = "block const";
var stillFunctionScoped = "still function";
}
// console.log(blockScoped); // ReferenceError
console.log(stillFunctionScoped); // Works
// Lexical scope
function inner() {
console.log(globalVar); // Access outer
console.log(functionScoped); // Access outer
}
inner();
}
testScope();
// Module scope (ES modules)
// Each file has its own scope
// export const moduleVar = "module";
// Strict mode scope
"use strict";
// var x = 10; // Error if not declaredundefined is a primitive value that indicates a variable has been declared but not assigned a value. It is the default value of uninitialized variables.
- Type: typeof undefined returns "undefined"
- Assignment: Automatically assigned to uninitialized variables
- Comparison: null == undefined (true), null === undefined (false)
- Use Case: Indicates missing value or property
// == vs === in JavaScript
// == (loose equality) - type coercion
console.log(5 == "5"); // true
console.log(true == 1); // true
console.log(null == undefined); // true
console.log(0 == false); // true
console.log("" == false); // true
// === (strict equality) - no type coercion
console.log(5 === "5"); // false
console.log(true === 1); // false
console.log(null === undefined); // false
console.log(0 === false); // false
console.log("" === false); // false
// Object comparison
console.log({} === {}); // false
console.log([] === []); // false
// NaN comparison
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
console.log(Object.is(NaN, NaN)); // true
// Best practice: Always use === unless you need type coercionThe typeof operator returns a string indicating the type of the operand. It is useful for type checking and debugging.
- Syntax:
typeof value - Returns: "string", "number", "boolean", "undefined", "object", "function", "symbol", "bigint"
- Special Cases: typeof null === "object"
- Use Case: Type checking and validation
// Callbacks in JavaScript
// Basic callback
function greet(name, callback) {
console.log(`Hello, ${name}`);
callback();
}
greet("Alice", function() {
console.log("Callback executed!");
});
// Callback with error handling
function fetchData(callback) {
try {
const data = { id: 1, name: "Alice" };
callback(null, data);
} catch (error) {
callback(error, null);
}
}
fetchData((error, data) => {
if (error) {
console.error("Error:", error);
} else {
console.log("Data:", data);
}
});
// Callback hell
doSomething(function(result1) {
doSomethingElse(result1, function(result2) {
doAnotherThing(result2, function(result3) {
console.log("Done!", result3);
});
});
});
// Solution: Promises or async/awaitNaN (Not-a-Number) is a special numeric value that indicates an invalid or unrepresentable number. It is returned when mathematical operations fail.
- Type: typeof NaN === "number"
- Comparison: NaN !== NaN (use isNaN() to check)
- Causes: Invalid math, parse errors
- Check:
isNaN()orNumber.isNaN()
// JSON in JavaScript
// JSON stringify and parse
const person = {
name: "Alice",
age: 25,
hobbies: ["reading", "coding"],
address: {
city: "NYC",
country: "USA"
}
};
// Convert object to JSON string
const jsonString = JSON.stringify(person);
console.log(jsonString);
// {"name":"Alice","age":25,"hobbies":["reading","coding"],"address":{"city":"NYC","country":"USA"}}
// Convert JSON string to object
const jsonObject = JSON.parse(jsonString);
console.log(jsonObject);
// Pretty print
const pretty = JSON.stringify(person, null, 2);
console.log(pretty);
// Replacer function
const filtered = JSON.stringify(person, (key, value) => {
return key === "age" ? undefined : value;
});
console.log(filtered);
// Reviver function
const revived = JSON.parse(jsonString, (key, value) => {
return key === "age" ? value + 10 : value;
});
console.log(revived);Strict mode is a way to opt-in to a restricted variant of JavaScript that catches common coding errors and prevents unsafe actions.
- Enable:
"use strict"at top of file or function - Benefits: Catches silent errors, prevents unsafe actions
- Changes: No global variables, no duplicate parameters
- Best Practice: Always use strict mode
// Promises in JavaScript
// Creating a promise
const myPromise = new Promise((resolve, reject) => {
// Async operation
setTimeout(() => {
const success = true;
if (success) {
resolve("Operation successful!");
} else {
reject("Operation failed!");
}
}, 1000);
});
// Using a promise
myPromise
.then(result => {
console.log("Success:", result);
return "Next step";
})
.then(nextResult => {
console.log("Next:", nextResult);
})
.catch(error => {
console.error("Error:", error);
})
.finally(() => {
console.log("Promise completed");
});
// Promise.all
const promises = [
Promise.resolve("First"),
Promise.resolve("Second"),
Promise.resolve("Third")
];
Promise.all(promises)
.then(results => {
console.log("All results:", results);
})
.catch(error => {
console.error("One failed:", error);
});
// Promise.race
Promise.race([
new Promise(resolve => setTimeout(() => resolve("Fast"), 100)),
new Promise(resolve => setTimeout(() => resolve("Slow"), 1000))
]).then(result => {
console.log("Race winner:", result);
});
// Promise.allSettled
Promise.allSettled([
Promise.resolve("Success"),
Promise.reject("Failure")
]).then(results => {
console.log("All settled:", results);
});Hoisting is JavaScript's behavior of moving declarations to the top of their scope during compilation. Variables and functions are hoisted differently.
- Function Declarations: Fully hoisted
- var Variables: Hoisted but initialized as undefined
- let/const Variables: Hoisted but in Temporal Dead Zone
- Function Expressions: Not hoisted
// Async/Await in JavaScript
// Basic async/await
async function fetchData() {
try {
const data = await new Promise((resolve) => {
setTimeout(() => resolve({ id: 1, name: "Alice" }), 1000);
});
console.log("Data:", data);
return data;
} catch (error) {
console.error("Error:", error);
throw error;
}
}
// Using async function
fetchData();
// Async with multiple awaits
async function processData() {
const data = await fetchData();
const processed = await process(data);
return processed;
}
// Parallel execution
async function parallelTasks() {
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts()
]);
return { user, posts };
}
// Error handling with async/await
async function safeFetch() {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Fetch failed:", error);
return null;
}
}A closure is a function that has access to variables from its outer (enclosing) scope even after the outer function has returned. It "closes over" the variables.
- Creation: Function defined inside another function
- Access: Can access outer function's variables
- Use Cases: Private variables, currying, event handlers
- Memory: Variables remain in memory as long as closure exists
// Event Bubbling and Delegation
// HTML structure
/*
<div id="parent">
<button id="child">Click me</button>
</div>
*/
// Event bubbling
document.getElementById("parent").addEventListener("click", () => {
console.log("Parent clicked (bubbling)");
}, false); // false = bubbling (default)
document.getElementById("child").addEventListener("click", () => {
console.log("Child clicked");
}, false);
// Event capturing
document.getElementById("parent").addEventListener("click", () => {
console.log("Parent clicked (capturing)");
}, true); // true = capturing
// Stop propagation
document.getElementById("child").addEventListener("click", (e) => {
e.stopPropagation();
console.log("Child clicked - propagation stopped");
});
// Event delegation
document.getElementById("parent").addEventListener("click", (e) => {
if (e.target.matches("button")) {
console.log("Button clicked via delegation:", e.target.id);
}
});
// Dynamic elements with delegation
document.addEventListener("click", (e) => {
if (e.target.classList.contains("dynamic-btn")) {
console.log("Dynamic button clicked");
}
});Scope determines the visibility and accessibility of variables in different parts of the code. JavaScript has global, function, and block scope.
- Global Scope: Variables accessible everywhere
- Function Scope: Variables accessible within function
- Block Scope: Variables accessible within block (let/const)
- Lexical Scope: Nested functions access parent scope
// this keyword in JavaScript
// Global context
console.log(this); // Window (browser)
// Function context (non-strict)
function showThis() {
console.log(this); // Window/global
}
showThis();
// Function context (strict)
"use strict";
function showThisStrict() {
console.log(this); // undefined
}
showThisStrict();
// Object method
const obj = {
name: "Alice",
greet() {
console.log(this.name);
}
};
obj.greet(); // Alice
// Arrow function (lexical this)
const arrowObj = {
name: "Bob",
greet: () => {
console.log(this.name); // undefined (lexical this)
}
};
arrowObj.greet();
// Constructor function
function Person(name) {
this.name = name;
}
const person1 = new Person("Alice");
console.log(person1.name); // Alice
// call, apply, bind
function introduce(greeting) {
console.log(`${greeting}, I'm ${this.name}`);
}
const user = { name: "Alice" };
introduce.call(user, "Hello"); // Hello, I'm Alice
introduce.apply(user, ["Hi"]); // Hi, I'm Alice
const bound = introduce.bind(user, "Hey");
bound(); // Hey, I'm Alice== (loose equality) compares values after type coercion. === (strict equality) compares both value and type without coercion.
- ==: Performs type coercion
- ===: No type coercion
- Best Practice: Always use === for predictable results
- Object Comparison: Both compare references, not values
// Arrow Functions in JavaScript
// Basic arrow function
const add = (a, b) => a + b;
// Arrow function with multiple statements
const multiply = (a, b) => {
const result = a * b;
return result;
};
// Arrow function with no parameters
const greet = () => "Hello!";
// Arrow function with one parameter (parens optional)
const double = x => x * 2;
// Arrow function returning object
const createPerson = (name, age) => ({ name, age });
// Arrow function in array methods
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(x => x * 2);
const evens = numbers.filter(x => x % 2 === 0);
const sum = numbers.reduce((acc, x) => acc + x, 0);
// Arrow function with rest parameters
const sumAll = (...nums) => nums.reduce((acc, n) => acc + n, 0);
// Arrow function with destructuring
const printName = ({ name }) => console.log(name);
// Arrow function lexical this
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
const timer = new Timer(); // Works correctly
// When NOT to use arrow functions
// 1. Object methods
const obj2 = {
name: "Alice",
greet: () => console.log(this.name) // Wrong!
};
// 2. Constructors
const Person2 = (name) => { this.name = name }; // Error
// 3. Event listeners (if you need 'this')
button.addEventListener("click", function() {
console.log(this); // button element
});A callback function is a function passed as an argument to another function and executed later, often after an asynchronous operation completes.
- Definition: Function passed as parameter
- Execution: Called inside the receiving function
- Use Cases: Event handlers, async operations, array methods
- Callback Hell: Nested callbacks leading to unreadable code
// Array Methods in JavaScript
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map - transform each element
const doubled = arr.map(x => x * 2);
console.log(doubled);
// filter - select elements
const evens = arr.filter(x => x % 2 === 0);
console.log(evens);
// reduce - aggregate
const sum = arr.reduce((acc, x) => acc + x, 0);
console.log(sum);
// forEach - iterate
arr.forEach(x => console.log(x));
// find - find first match
const first = arr.find(x => x > 5);
console.log(first);
// findIndex - find index of first match
const index = arr.findIndex(x => x > 5);
console.log(index);
// some - check if any match
const hasEven = arr.some(x => x % 2 === 0);
console.log(hasEven);
// every - check if all match
const allEven = arr.every(x => x % 2 === 0);
console.log(allEven);
// includes - check if value exists
const includes = arr.includes(5);
console.log(includes);
// sort - sort array
const sorted = [...arr].sort((a, b) => a - b);
console.log(sorted);
// reverse - reverse array
const reversed = [...arr].reverse();
console.log(reversed);
// slice - create subarray
const sliced = arr.slice(2, 5);
console.log(sliced);
// splice - modify array
const spliced = [...arr];
spliced.splice(2, 3, 99, 100);
console.log(spliced);
// concat - merge arrays
const merged = arr.concat([11, 12, 13]);
console.log(merged);
// flat - flatten nested arrays
const nested = [1, [2, 3], [4, [5, 6]]];
const flattened = nested.flat(2);
console.log(flattened);JSON (JavaScript Object Notation) is a lightweight data interchange format that is easy for humans to read and write, and easy for machines to parse and generate.
- Format: Key-value pairs, arrays, nested objects
- Methods:
JSON.stringify()andJSON.parse() - Data Types: Strings, numbers, booleans, arrays, objects, null
- Use Cases: API communication, configuration files
// Object Methods in JavaScript
const person = {
name: "Alice",
age: 25,
city: "NYC",
hobbies: ["reading", "coding"]
};
// Object.keys - get keys
const keys = Object.keys(person);
console.log(keys);
// Object.values - get values
const values = Object.values(person);
console.log(values);
// Object.entries - get key-value pairs
const entries = Object.entries(person);
console.log(entries);
// Object.fromEntries - create from entries
const fromEntries = Object.fromEntries(entries);
console.log(fromEntries);
// Object.assign - merge objects
const additional = { country: "USA", age: 26 };
const merged = Object.assign({}, person, additional);
console.log(merged);
// Object.freeze - prevent modifications
const frozen = Object.freeze({ name: "Alice" });
// frozen.name = "Bob"; // Error in strict mode
// Object.seal - prevent adding/removing properties
const sealed = Object.seal({ name: "Alice" });
sealed.name = "Bob"; // Works
// sealed.age = 25; // Error
// Object.hasOwn - check own property
console.log(Object.hasOwn(person, "name")); // true
console.log(Object.hasOwn(person, "toString")); // false
// Object.getOwnPropertyNames
const ownProps = Object.getOwnPropertyNames(person);
console.log(ownProps);A promise is an object representing the eventual completion or failure of an asynchronous operation. It provides a cleaner way to handle async code than callbacks.
- States: pending, fulfilled, rejected
- Methods:
then(),catch(),finally() - Chaining: Sequential async operations
- Static Methods:
Promise.all(),Promise.race()
// Destructuring in JavaScript
// Array destructuring
const arr = [1, 2, 3, 4, 5];
const [first, second, ...rest] = arr;
console.log(first, second, rest); // 1, 2, [3, 4, 5]
// Swap variables
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2, 1
// Object destructuring
const person = { name: "Alice", age: 25, city: "NYC" };
const { name, age } = person;
console.log(name, age);
// Rename variables
const { name: fullName, age: years } = person;
console.log(fullName, years);
// Default values
const { country = "USA" } = person;
console.log(country);
// Nested destructuring
const user = {
id: 1,
profile: {
name: "Alice",
address: {
city: "NYC",
zip: "10001"
}
}
};
const { profile: { address: { city } } } = user;
console.log(city);
// Function parameter destructuring
function printPerson({ name, age }) {
console.log(`${name} is ${age} years old`);
}
printPerson(person);
// Array destructuring with rest
const [head, ...tail] = [1, 2, 3, 4];
console.log(head, tail);Async/await is modern syntax for working with promises, making asynchronous code look and behave like synchronous code.
- async: Declares an asynchronous function
- await: Waits for a promise to resolve
- Error Handling: Use try/catch
- Readability: Cleaner than promise chains
// Spread and Rest Operators
// Spread operator in arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined);
// Copy array
const copy = [...arr1];
console.log(copy);
// Spread in function calls
const numbers = [1, 2, 3, 4, 5];
const max = Math.max(...numbers);
console.log(max);
// Spread in objects
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const mergedObj = { ...obj1, ...obj2 };
console.log(mergedObj);
// Rest parameters
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3, 4));
// Rest in destructuring
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head, tail);
// Rest in object destructuring
const { a, ...restObj } = { a: 1, b: 2, c: 3 };
console.log(a, restObj);
// Spread with strings
const chars = [..."hello"];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']
// Spread with sets
const set = new Set([1, 2, 3]);
const arr = [...set];
console.log(arr);Event bubbling is the propagation of an event from the target element up through the DOM tree to the root. It allows parent elements to handle events from children.
- Direction: Child → Parent → Root
- Stop:
stopPropagation() - Prevent:
preventDefault() - Capturing Phase: Opposite direction (root → child)
// Template Literals in JavaScript
// Basic template literal
const name = "Alice";
const greeting = `Hello, ${name}!`;
console.log(greeting);
// Multi-line strings
const multiLine = `
This is a
multi-line
string
`;
console.log(multiLine);
// Expression interpolation
const a = 5, b = 10;
console.log(`${a} + ${b} = ${a + b}`);
// Nested templates
const isAdmin = true;
const message = `User is ${isAdmin ? `an admin` : `a regular user`}`;
console.log(message);
// Tagged templates
function highlight(strings, ...values) {
return strings.reduce((acc, str, i) => {
return acc + str + (values[i] ? `<b>${values[i]}</b>` : "");
}, "");
}
const highlighted = highlight`Hello ${name}, you are ${age} years old`;
console.log(highlighted);
// Raw strings
const raw = String.raw`Hello\nWorld`;
console.log(raw); // Hello\nWorld (not escaped)
// Template literals in loops
const items = ["Apple", "Banana", "Orange"];
const list = items.map(item => `<li>${item}</li>`).join("");
console.log(list);Event delegation is a technique where a parent element handles events for its children using event bubbling. It reduces the number of event listeners needed.
- Benefits: Fewer listeners, handles dynamic elements
- Implementation: Add listener to parent
- Target:
event.targetto identify child - Use Cases: Lists, tables, dynamic content
// Map and Set in JavaScript
// Map - key-value pairs (any keys)
const map = new Map();
// Set values
map.set("name", "Alice");
map.set(42, "answer");
map.set({ id: 1 }, "object key");
// Get values
console.log(map.get("name")); // Alice
console.log(map.get(42)); // answer
// Check key existence
console.log(map.has("name")); // true
// Delete key
map.delete("name");
// Iterate map
map.forEach((value, key) => {
console.log(key, value);
});
// Map from array
const mapFromArray = new Map([
["name", "Alice"],
["age", 25]
]);
console.log(mapFromArray);
// Set - unique values
const set = new Set();
// Add values
set.add(1);
set.add(2);
set.add(3);
set.add(3); // Duplicate ignored
console.log(set.has(2)); // true
console.log(set.size); // 3
// Delete value
set.delete(2);
// Iterate set
set.forEach(value => console.log(value));
// Set from array
const setFromArray = new Set([1, 2, 3, 3, 4]);
console.log(setFromArray); // Set(4) {1, 2, 3, 4}
// Remove duplicates from array
const arr = [1, 2, 3, 3, 4, 4, 5];
const unique = [...new Set(arr)];
console.log(unique);The this keyword refers to the object that is currently executing the function. Its value depends on how the function is called.
- Global: Window (browser) or Global (Node)
- Method: Owner object
- Constructor: New instance
- Arrow Function: Lexical (parent's this)
// WeakMap and WeakSet
// WeakMap - keys must be objects, garbage collected
const weakMap = new WeakMap();
const obj = { id: 1 };
weakMap.set(obj, "value");
console.log(weakMap.get(obj));
// WeakMap does not prevent garbage collection
// If obj is deleted, the entry is automatically removed
// WeakSet - values must be objects
const weakSet = new WeakSet();
const obj2 = { id: 2 };
weakSet.add(obj2);
console.log(weakSet.has(obj2));
// Use cases: caching, private data
// Private data with WeakMap
const privateData = new WeakMap();
class Person {
constructor(name) {
privateData.set(this, { name });
}
getName() {
return privateData.get(this).name;
}
}
const p = new Person("Alice");
console.log(p.getName());
// WeakMap for DOM element metadata
const elementData = new WeakMap();
const button = document.createElement("button");
elementData.set(button, { clicks: 0 });
button.addEventListener("click", () => {
const data = elementData.get(button);
data.clicks++;
console.log(`Clicked ${data.clicks} times`);
});Arrow functions provide a shorter syntax for writing functions and have lexical this binding. They are commonly used for callbacks and functional programming.
- Syntax:
() => {} - this: Lexical binding (parent's this)
- No arguments: Use rest parameters
- No constructor: Cannot be used with
new
// Symbol in JavaScript
// Creating symbols
const sym1 = Symbol();
const sym2 = Symbol("description");
const sym3 = Symbol("description");
console.log(sym2 === sym3); // false
// Symbols as object keys
const uniqueKey = Symbol("key");
const obj = {
[uniqueKey]: "secret value",
regular: "normal"
};
console.log(obj[uniqueKey]); // secret value
// Symbol.for - global symbol registry
const globalSym1 = Symbol.for("shared");
const globalSym2 = Symbol.for("shared");
console.log(globalSym1 === globalSym2); // true
// Well-known symbols
// Symbol.iterator
const iterableObj = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
};
for (const value of iterableObj) {
console.log(value);
}
// Symbol.toStringTag
const customObj = {
[Symbol.toStringTag]: "CustomObject"
};
console.log(Object.prototype.toString.call(customObj)); // [object CustomObject]
// Symbol.toPrimitive
const primitiveObj = {
[Symbol.toPrimitive](hint) {
if (hint === "number") return 42;
if (hint === "string") return "forty-two";
return null;
}
};
console.log(+primitiveObj); // 42
console.log(`${primitiveObj}`); // forty-twoThe map() method creates a new array by applying a function to each element of the original array. It does not modify the original array.
- Syntax:
array.map(callback) - Return: New array with transformed elements
- Parameters: element, index, array
- Chaining: Can chain with other array methods
// Iterators and Generators
// Custom iterator
const range = {
start: 0,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const num of range) {
console.log(num);
}
// Generator function
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
const gen = numberGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
// Infinite generator
function* infiniteGenerator() {
let i = 0;
while (true) {
yield i++;
}
}
const infinite = infiniteGenerator();
console.log(infinite.next().value); // 0
console.log(infinite.next().value); // 1
console.log(infinite.next().value); // 2
// Generator with return
function* generatorWithReturn() {
yield 1;
yield 2;
return 3;
yield 4;
}
const gen2 = generatorWithReturn();
console.log(gen2.next()); // { value: 1, done: false }
console.log(gen2.next()); // { value: 2, done: false }
console.log(gen2.next()); // { value: 3, done: true }
// Generator delegation
function* generator1() {
yield 1;
yield 2;
}
function* generator2() {
yield* generator1();
yield 3;
yield 4;
}
for (const value of generator2()) {
console.log(value); // 1, 2, 3, 4
}The filter() method creates a new array with elements that pass a test implemented by a provided function. It does not modify the original array.
- Syntax:
array.filter(callback) - Return: New array with elements that pass the test
- Parameters: element, index, array
- Use Cases: Removing unwanted elements
// Classes in JavaScript
// Class definition
class Person {
// Constructor
constructor(name, age) {
this.name = name;
this.age = age;
}
// Instance method
greet() {
return `Hello, I'm ${this.name}`;
}
// Getter
get fullName() {
return `${this.name} (Age: ${this.age})`;
}
// Setter
set fullName(value) {
this.name = value;
}
// Static method
static createAnonymous() {
return new Person("Anonymous", 0);
}
// Private field (ES2022)
#privateField = "private";
// Private method
#privateMethod() {
return "private method";
}
}
// Inheritance
class Student extends Person {
constructor(name, age, grade) {
super(name, age);
this.grade = grade;
}
// Override method
greet() {
return `${super.greet()} and I'm in grade ${this.grade}`;
}
}
const person = new Person("Alice", 25);
console.log(person.greet());
console.log(person.fullName);
const student = new Student("Bob", 20, "A");
console.log(student.greet());
// Static method
const anonymous = Person.createAnonymous();
console.log(anonymous.greet());
// Class expression
const Animal = class {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
};The reduce() method reduces an array to a single value by executing a reducer function on each element. It accumulates the result.
- Syntax:
array.reduce(callback, initialValue) - Parameters: accumulator, current, index, array
- Return: Single accumulated value
- Use Cases: Sum, average, object grouping
// Modules in JavaScript (ES6)
// Exporting
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// Default export
export default class Calculator {
multiply(a, b) { return a * b; }
}
// Named export with alias
export { add as sum };
// Importing
// main.js
import Calculator, { PI, add, subtract, sum } from './math.js';
// Import all
import * as MathUtils from './math.js';
// Import with alias
import { add as addition } from './math.js';
// Dynamic import
const module = await import('./math.js');
// Import for side effects
import './styles.css';
// Re-exporting
export { add, subtract } from './math.js';
export * from './math.js';
// Module scope
// Variables in modules are scoped to the module
// "use strict" is applied automatically
// Module loading
// <script type="module" src="main.js"></script>
// <script type="module">
// import { add } from './math.js';
// </script>
// Top-level await
const data = await fetch('/api/data');
const json = await data.json();
console.log(json);The prototype is a mechanism for sharing properties and methods between objects in JavaScript. Every function has a prototype property.
- Prototype Chain: Objects inherit from their prototype
- __proto__: Object's prototype reference
- Prototype Property:
Object.prototype - Inheritance: Shared methods and properties
// Prototypes in JavaScript
// Prototype chain
const parent = { name: "Parent", greet() { return "Hello"; } };
const child = Object.create(parent);
child.name = "Child";
console.log(child.name); // Child
console.log(child.greet()); // Hello (inherited)
// Constructor function
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
const person2 = new Person("Alice", 25);
console.log(person2.greet());
// Prototype inheritance
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.study = function() {
return `${this.name} is studying`;
};
const student2 = new Student("Bob", 20, "A");
console.log(student2.greet());
console.log(student2.study());
// hasOwnProperty
console.log(person2.hasOwnProperty("name")); // true
console.log(person2.hasOwnProperty("greet")); // false
// __proto__ (deprecated)
console.log(person2.__proto__ === Person.prototype); // true
// Object.getPrototypeOf
console.log(Object.getPrototypeOf(person2) === Person.prototype); // true
// instanceof
console.log(person2 instanceof Person); // true
console.log(person2 instanceof Object); // truePrototypal inheritance is a form of inheritance where objects inherit properties and methods from other objects (their prototype).
- Prototype Chain: Objects inherit from their prototype
- Object.create: Create objects with specific prototype
- Classes: ES6 class syntax wraps prototype inheritance
- Mixins: Combining multiple prototypes
// Prototypal Inheritance
// Object.create for inheritance
const animal = {
speak() {
return `${this.name} makes a sound`;
},
eat() {
return `${this.name} is eating`;
}
};
const dog = Object.create(animal);
dog.name = "Rex";
dog.speak = function() {
return `${this.name} barks!`;
};
console.log(dog.speak()); // Rex barks!
console.log(dog.eat()); // Rex is eating
// Multiple inheritance with mixins
const flyable = {
fly() {
return `${this.name} is flying`;
}
};
const swimmable = {
swim() {
return `${this.name} is swimming`;
}
};
function mixin(target, ...sources) {
Object.assign(target, ...sources);
}
const duck = { name: "Donald" };
mixin(duck, flyable, swimmable);
console.log(duck.fly()); // Donald is flying
console.log(duck.swim()); // Donald is swimming
// Class-based inheritance (ES6)
class Animal2 {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Dog2 extends Animal2 {
speak() {
return `${this.name} barks!`;
}
}
const rex = new Dog2("Rex");
console.log(rex.speak()); // Rex barks!Currying is the technique of transforming a function that takes multiple arguments into a sequence of functions that each take a single argument.
- Benefits: Partial application, function composition
- Implementation: Return functions for each argument
- Use Cases: Configuration, reusable functions
- Arrow Functions:
a => b => a + b
// Currying in JavaScript
// Basic currying
function add(a) {
return function(b) {
return a + b;
};
}
const add5 = add(5);
console.log(add5(3)); // 8
console.log(add(5)(3)); // 8
// Currying with arrow functions
const multiply = a => b => a * b;
const multiplyBy2 = multiply(2);
console.log(multiplyBy2(5)); // 10
// Currying with multiple arguments
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...more) {
return curried.apply(this, args.concat(more));
};
};
}
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
// Practical example: discount calculation
function calculateDiscount(discount) {
return function(price) {
return price * (1 - discount);
};
}
const tenPercentOff = calculateDiscount(0.10);
const twentyPercentOff = calculateDiscount(0.20);
console.log(tenPercentOff(100)); // 90
console.log(twentyPercentOff(100)); // 80
// Partial application
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = greet.bind(null, "Hello");
console.log(sayHello("Alice")); // Hello, Alice!Debouncing is a technique that limits how often a function is called by delaying its execution until after a specified time has passed since the last call.
- Use Cases: Search input, resize events
- Implementation: setTimeout and clearTimeout
- Delay: Function executes after delay
- Benefits: Reduces unnecessary function calls
// Debouncing in JavaScript
// Debounce function
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage: debounced search
const searchInput = document.getElementById("search");
const debouncedSearch = debounce((query) => {
console.log("Searching for:", query);
// API call here
}, 500);
searchInput.addEventListener("input", (e) => {
debouncedSearch(e.target.value);
});
// Debounce with immediate execution
function debounceImmediate(func, delay, immediate = false) {
let timeoutId;
return function(...args) {
const callNow = immediate && !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
if (!immediate) {
func.apply(this, args);
}
}, delay);
if (callNow) {
func.apply(this, args);
}
};
}
// Usage: debounced save
const saveButton = document.getElementById("save");
const debouncedSave = debounceImmediate(() => {
console.log("Saving data...");
}, 1000, true);
saveButton.addEventListener("click", debouncedSave);
// Debounce with leading and trailing options
function debounceAdvanced(func, delay, options = { leading: false, trailing: true }) {
let timeoutId;
let lastCallTime;
return function(...args) {
const now = Date.now();
const isFirstCall = !lastCallTime;
lastCallTime = now;
if (options.leading && isFirstCall) {
func.apply(this, args);
return;
}
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
if (options.trailing) {
func.apply(this, args);
}
}, delay);
};
}Throttling is a technique that limits how often a function can be called by ensuring it executes at most once in a specified time period.
- Use Cases: Scroll events, animations
- Implementation: Timestamps or setTimeout
- Rate Limit: Function executes at fixed intervals
- Benefits: Prevents performance issues
// Throttling in JavaScript
// Throttle function
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Usage: throttled scroll
const throttledScroll = throttle(() => {
console.log("Scroll position:", window.scrollY);
}, 200);
window.addEventListener("scroll", throttledScroll);
// Throttle with trailing execution
function throttleTrailing(func, limit) {
let inThrottle;
let lastFunc;
let lastRan;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
lastRan = Date.now();
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func.apply(this, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
// Throttle with leading and trailing options
function throttleAdvanced(func, limit, options = { leading: true, trailing: true }) {
let inThrottle;
let lastRan;
let lastFunc;
return function(...args) {
const now = Date.now();
if (!inThrottle) {
if (options.leading) {
func.apply(this, args);
}
lastRan = now;
inThrottle = true;
setTimeout(() => {
inThrottle = false;
if (options.trailing && lastFunc) {
func.apply(this, args);
lastFunc = null;
}
}, limit);
} else {
if (options.trailing) {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
func.apply(this, args);
}, limit - (now - lastRan));
}
}
};
}The event loop is a mechanism that handles asynchronous callbacks in JavaScript. It continuously checks the call stack and task queues to execute pending tasks.
- Call Stack: Synchronous execution
- Task Queue: Asynchronous callbacks
- Microtasks: Promise callbacks (higher priority)
- Macrotasks: setTimeout, setInterval, events
// Event Loop in JavaScript
// Basic event loop example
console.log("Start");
setTimeout(() => {
console.log("Timeout callback");
}, 0);
Promise.resolve().then(() => {
console.log("Promise callback");
});
console.log("End");
// Output:
// Start
// End
// Promise callback
// Timeout callback
// Microtasks vs Macrotasks
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
// Output: 1, 4, 3, 2
// Microtasks (Promises) execute before Macrotasks (setTimeout)
// Event loop with async/await
async function asyncFunction() {
console.log("A");
await Promise.resolve();
console.log("B");
}
console.log("C");
asyncFunction();
console.log("D");
// Output: C, A, D, B
// Event loop visualization
setTimeout(() => console.log("Timeout 1"), 0);
setTimeout(() => console.log("Timeout 2"), 100);
Promise.resolve()
.then(() => console.log("Promise 1"))
.then(() => console.log("Promise 2"));
console.log("Sync code");
// Output: Sync code, Promise 1, Promise 2, Timeout 1, Timeout 2call(), apply(), and bind() are methods that allow you to control the value of this in a function.
- call(): Calls function with specific
thisand arguments - apply(): Same as call but takes array of arguments
- bind(): Creates new function with specific
this - Use Cases: Method borrowing, partial application
// call(), apply(), bind() Methods
const person = {
name: "Alice",
greet(greeting) {
return `${greeting}, I'm ${this.name}`;
}
};
// call()
const bob = { name: "Bob" };
console.log(person.greet.call(bob, "Hello")); // Hello, I'm Bob
// apply()
console.log(person.greet.apply(bob, ["Hi"])); // Hi, I'm Bob
// bind()
const greetBob = person.greet.bind(bob);
console.log(greetBob("Hey")); // Hey, I'm Bob
// Partial application with bind
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
console.log(double(5)); // 10
// Borrowing methods
const arr = [1, 2, 3];
const arrLike = { 0: "a", 1: "b", 2: "c", length: 3 };
const result = Array.prototype.slice.call(arrLike);
console.log(result); // ['a', 'b', 'c']
// Using apply with Math.max
const numbers = [1, 5, 3, 9, 2];
const max = Math.max.apply(null, numbers);
console.log(max); // 9
// Using bind with event handlers
class Button {
constructor(text) {
this.text = text;
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(`${this.text} clicked`);
}
}
const btn = new Button("Save");
// btn.handleClick(); // Save clickedA memory leak occurs when memory that is no longer needed is not released. JavaScript's garbage collector handles most memory, but leaks can still happen.
- Causes: Global variables, forgotten timers, closures
- Detection: Browser DevTools memory profiler
- Prevention: Clean up, use WeakMap/WeakSet
- Impact: Performance degradation, crashes
// Memory Leaks in JavaScript
// Common memory leak sources
// 1. Global variables
function leak() {
globalVar = "I'm global"; // Implicit global
}
leak();
// 2. Forgotten timers
function timerLeak() {
let element = document.getElementById("leak");
setInterval(() => {
console.log(element.id);
}, 1000);
}
// 3. Event listeners not removed
function eventListenerLeak() {
let element = document.getElementById("leak");
element.addEventListener("click", function handler() {
console.log("Clicked");
});
// element removed but listener remains
}
// 4. Closures holding references
function closureLeak() {
let largeData = new Array(1000000);
return function() {
console.log(largeData.length);
};
}
const leakyClosure = closureLeak();
// 5. DOM references
function domReferenceLeak() {
let element = document.getElementById("leak");
document.body.removeChild(element);
// element still holds reference
}
// Prevention
// 1. Use 'use strict'
// 2. Clear timers
clearTimeout(timeoutId);
clearInterval(intervalId);
// 3. Remove event listeners
element.removeEventListener("click", handler);
// 4. Nullify references
element = null;
// 5. Use WeakMap and WeakSet
const cache = new WeakMap();
const obj = {};
cache.set(obj, "value");
// When obj is garbage collected, entry is removedA module is a reusable piece of code that exports specific functionality and can be imported by other modules. ES6 introduced native module support.
- Export:
exportkeyword - Import:
importkeyword - Default Export:
export default - Dynamic Import:
import()for lazy loading
// ES6 Modules Advanced
// module.js
export const data = { id: 1, name: "Alice" };
export function process() { return "processing"; }
// Default export
export default class User {
constructor(name) { this.name = name; }
}
// Import with alias
import User, { data as userData, process } from './module.js';
// Re-exporting
export { data, process } from './module.js';
export * from './module.js';
// Import namespace
import * as Module from './module.js';
console.log(Module.data);
// Dynamic import (lazy loading)
async function loadModule() {
const module = await import('./module.js');
console.log(module.default);
}
// Module import for side effects
import './styles.css';
// JSON imports (with import assertions)
import data from './data.json' assert { type: "json" };
console.log(data);
// Module variables
console.log(import.meta.url); // Current module URL
console.log(import.meta); // Module metadata
// Exporting types
export interface User {
name: string;
age: number;
}
// Export with dynamic binding
export let counter = 0;
export function increment() {
counter++;
}IIFE (Immediately Invoked Function Expression) is a function that is defined and executed immediately after creation. It creates a private scope.
- Syntax:
(function() )() - Purpose: Private variables, module pattern
- Arrow IIFE:
(() => {})() - Use Cases: Module pattern, avoiding global scope
// IIFE (Immediately Invoked Function Expression)
// Basic IIFE
(function() {
console.log("IIFE executed!");
})();
// IIFE with parameters
(function(name) {
console.log(`Hello, ${name}!`);
})("Alice");
// Arrow function IIFE
(() => {
console.log("Arrow IIFE");
})();
// IIFE with return value
const result = (function() {
let count = 0;
return {
increment() { count++; },
decrement() { count--; },
getCount() { return count; }
};
})();
// IIFE for private variables
const counterModule = (function() {
let privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment() { changeBy(1); },
decrement() { changeBy(-1); },
value() { return privateCounter; }
};
})();
// IIFE for module pattern
const myModule = (function() {
// Private
const privateData = [];
// Public
return {
add(item) {
privateData.push(item);
},
get() {
return [...privateData];
}
};
})();
// IIFE in loops (pre-ES6)
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(() => {
console.log(index);
}, 100);
})(i);
}
// With let (no IIFE needed)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}To reverse a string, you can split it into an array, reverse the array, and join it back together. This is a common interview question.
- Method:
str.split('').reverse().join('') - Alternative:
[...str].reverse().join('') - Loop Method: Iterate from end to start
- Recursive: Recursively reverse the string
// Reverse a string
function reverseString(str) {
return str.split('').reverse().join('');
}
console.log(reverseString("hello")); // "olleh"
// Alternative with spread
const reverse = str => [...str].reverse().join('');
console.log(reverse("world")); // "dlrow"A palindrome is a word that reads the same backward as forward. Check by reversing the string and comparing with the original.
- Method:
str === str.split('').reverse().join('') - Case Insensitive: Convert to lowercase
- Ignore Non-alphanumeric: Use regex
- Two-pointer: Compare from both ends
// Check palindrome
function isPalindrome(str) {
const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '');
return cleaned === cleaned.split('').reverse().join('');
}
console.log(isPalindrome("racecar")); // true
console.log(isPalindrome("hello")); // falseFind the maximum value in an array using Math.max with the spread operator, or by iterating through the array.
- Method:
Math.max(...arr) - Loop:
arr.reduce((max, x) => Math.max(max, x)) - Sorted:
arr[arr.length - 1] - Edge Cases: Handle empty array
// Find max in array
function findMax(arr) {
return Math.max(...arr);
}
console.log(findMax([1, 5, 3, 9, 2])); // 9
// Without spread
function findMaxManual(arr) {
return arr.reduce((max, curr) => curr > max ? curr : max, -Infinity);
}Remove duplicates from an array using Set, filter, or reduce. The Set method is the simplest and most efficient.
- Set:
[...new Set(arr)] - Filter:
arr.filter((item, i) => arr.indexOf(item) === i) - Reduce:
arr.reduce((acc, x) => acc.includes(x) ? acc : [...acc, x], []) - Use Cases: Unique values, deduplication
// Remove duplicates
function removeDuplicates(arr) {
return [...new Set(arr)];
}
console.log(removeDuplicates([1, 2, 2, 3, 3, 4])); // [1, 2, 3, 4]
// With filter
function removeDuplicatesFilter(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}Merge two arrays using spread operator or concat method. Both create a new array without modifying the originals.
- Spread:
[...arr1, ...arr2] - Concat:
arr1.concat(arr2) - Push:
arr1.push(...arr2)(modifies arr1) - Unique Merge:
[...new Set([...arr1, ...arr2])]
// Merge arrays
function mergeArrays(arr1, arr2) {
return [...arr1, ...arr2];
}
console.log(mergeArrays([1, 2], [3, 4])); // [1, 2, 3, 4]
// Alternative
function mergeArraysConcat(arr1, arr2) {
return arr1.concat(arr2);
}Convert a string to a number using Number(), parseInt(), parseFloat(), or the unary plus operator.
- Number():
Number(str) - parseInt():
parseInt(str, 10) - parseFloat():
parseFloat(str) - Unary Plus:
+str
// Convert string to number
function stringToNumber(str) {
return Number(str);
}
console.log(stringToNumber("42")); // 42
// Alternative
function stringToNumberParse(str) {
return parseInt(str, 10);
}
console.log(stringToNumberParse("42")); // 42Loop through an object's properties using for...in, Object.keys(), Object.values(), or Object.entries().
- for...in:
for (let key in obj) - Object.keys():
Object.keys(obj).forEach(key => ...) - Object.values():
Object.values(obj).forEach(value => ...) - Object.entries():
Object.entries(obj).forEach(([key, value]) => ...)
// Loop through object
function loopObject(obj) {
// for...in
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key, obj[key]);
}
}
// Object.keys
Object.keys(obj).forEach(key => {
console.log(key, obj[key]);
});
// Object.entries
Object.entries(obj).forEach(([key, value]) => {
console.log(key, value);
});
}
const person = { name: "Alice", age: 25 };
loopObject(person);Delay function execution using setTimeout, async/await with sleep, or setInterval for repeated execution.
- setTimeout:
setTimeout(func, ms) - Promise:
new Promise(resolve => setTimeout(resolve, ms)) - async/await:
await sleep(ms) - setInterval:
setInterval(func, ms)
// Delay function execution
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function delayedExecution() {
console.log("Start");
await delay(2000);
console.log("After 2 seconds");
}
delayedExecution();
// SetTimeout
function delayCallback(callback, ms) {
setTimeout(callback, ms);
}
delayCallback(() => console.log("Delayed"), 1000);Fetch API provides a modern way to make HTTP requests. It returns promises and supports async/await syntax.
- GET:
fetch(url).then(res => res.json()) - POST:
fetch(url, { method: "POST", body: JSON.stringify(data) }) - Async/Await:
const data = await fetch(url).then(res => res.json()) - Error Handling:
if (!response.ok) throw new Error()
// Fetch API example
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
// POST request
async function postData(url, data) {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
return await response.json();
} catch (error) {
console.error('POST error:', error);
throw error;
}
}
// With AbortController
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
fetch('https://api.example.com/data', {
signal: controller.signal
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request aborted');
}
})
.finally(() => clearTimeout(timeoutId));Create a promise using the Promise constructor with resolve and reject functions. Use it for async operations.
- Constructor:
new Promise((resolve, reject) => {}) - resolve(): Fulfill the promise
- reject(): Reject the promise
- Then:
promise.then(callback).catch(callback)
// Create a promise
function createPromise(shouldResolve) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldResolve) {
resolve("Success!");
} else {
reject("Failed!");
}
}, 1000);
});
}
// Using the promise
createPromise(true)
.then(result => console.log("Resolved:", result))
.catch(error => console.error("Rejected:", error));
// Promise with parameters
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: `User${id}` });
} else {
reject("Invalid ID");
}
}, 1000);
});
}
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error(error));Calculate factorial using recursion or iteration. Factorial of n is the product of all positive integers less than or equal to n.
- Recursive:
n <= 1 ? 1 : n * factorial(n-1) - Iterative: Loop from 2 to n
- Edge Cases: 0! = 1, negative numbers
- Performance: Iterative is faster
// Factorial
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
// Iterative
function factorialIterative(n) {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}Calculate Fibonacci numbers using recursion, iteration, or memoization. The Fibonacci sequence starts with 0, 1, 1, 2, 3, 5, 8, ...
- Recursive:
n <= 1 ? n : fib(n-1) + fib(n-2) - Iterative: Loop with variables
- Memoization: Cache results for performance
- Time Complexity: O(2^n) recursive, O(n) iterative
// Fibonacci
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(8)); // 21
// Iterative
function fibonacciIterative(n) {
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return n ? b : a;
}FizzBuzz prints numbers from 1 to n, but for multiples of 3 print "Fizz", for multiples of 5 print "Buzz", and for multiples of both print "FizzBuzz".
- Logic: Check divisibility by 3 and 5
- Order: Check 15 first, then 3, then 5
- Output: Print each result
- Use Cases: Common interview question
// FizzBuzz
function fizzBuzz(n) {
for (let i = 1; i <= n; i++) {
if (i % 15 === 0) console.log("FizzBuzz");
else if (i % 3 === 0) console.log("Fizz");
else if (i % 5 === 0) console.log("Buzz");
else console.log(i);
}
}
fizzBuzz(15);Find the missing number in an array of consecutive integers. Use the formula n*(n+1)/2 - sum of array.
- Formula:
total = n * (n + 1) / 2 - Missing:
total - sum(arr) - XOR Method: XOR all numbers and indices
- Edge Cases: Empty array, missing first/last
// Find missing number
function findMissing(arr) {
const n = arr.length + 1;
const total = n * (n + 1) / 2;
const sum = arr.reduce((acc, x) => acc + x, 0);
return total - sum;
}
console.log(findMissing([1, 2, 4, 5, 6])); // 3Find duplicate elements in an array using Set, Map, or filter. The Set method is the most efficient.
- Set:
new Set(arr.filter(x => arr.indexOf(x) !== arr.lastIndexOf(x))) - Map: Track frequency with Map
- Filter:
arr.filter((x, i) => arr.indexOf(x) !== i) - Time Complexity: O(n) with Set
// Find duplicates
function findDuplicates(arr) {
const seen = new Set();
const duplicates = new Set();
for (const item of arr) {
if (seen.has(item)) {
duplicates.add(item);
} else {
seen.add(item);
}
}
return [...duplicates];
}
console.log(findDuplicates([1, 2, 3, 2, 4, 3])); // [2, 3]Calculate the sum of all elements in an array using reduce, for loop, or forEach method.
- Reduce:
arr.reduce((acc, x) => acc + x, 0) - For Loop:
let sum = 0; for (let x of arr) sum += x; - forEach:
arr.forEach(x => sum += x) - Edge Cases: Empty array returns 0
// Sum of array
function sumArray(arr) {
return arr.reduce((acc, x) => acc + x, 0);
}
console.log(sumArray([1, 2, 3, 4, 5])); // 15Calculate the average of an array by dividing the sum by the array length. Handle empty arrays.
- Method:
sum / arr.length - Empty Array: Return 0 or null
- Reduce:
arr.reduce((a, x) => a + x, 0) / arr.length - Use Cases: Statistics, data analysis
// Average of array
function averageArray(arr) {
return arr.reduce((acc, x) => acc + x, 0) / arr.length;
}
console.log(averageArray([1, 2, 3, 4, 5])); // 3Sort an array in ascending order using the sort method with a comparator function. Numbers need a custom comparator.
- Method:
arr.sort((a, b) => a - b) - Spread:
[...arr].sort((a, b) => a - b) - Strings:
arr.sort()(works for strings) - Edge Cases: Negative numbers, decimals
// Sort array ascending
function sortAscending(arr) {
return [...arr].sort((a, b) => a - b);
}
console.log(sortAscending([5, 2, 8, 1, 9])); // [1, 2, 5, 8, 9]Sort an array in descending order using the sort method with a comparator function that subtracts in reverse.
- Method:
arr.sort((a, b) => b - a) - Spread:
[...arr].sort((a, b) => b - a) - Reverse:
arr.sort((a, b) => a - b).reverse() - Edge Cases: Negative numbers, decimals
// Sort array descending
function sortDescending(arr) {
return [...arr].sort((a, b) => b - a);
}
console.log(sortDescending([5, 2, 8, 1, 9])); // [9, 8, 5, 2, 1]Flatten a nested array using recursion, reduce, or the flat() method. Handle multiple levels of nesting.
- flat():
arr.flat(Infinity) - Recursive:
arr.reduce((acc, x) => acc.concat(Array.isArray(x) ? flatten(x) : x), []) - Stack: Use a stack for iterative flattening
- Depth: Specify depth or flatten completely
// Flatten nested array
function flattenArray(arr) {
return arr.reduce((acc, val) =>
Array.isArray(val) ? acc.concat(flattenArray(val)) : acc.concat(val),
[]
);
}
console.log(flattenArray([1, [2, [3, 4], 5], 6])); // [1, 2, 3, 4, 5, 6]
// ES2019 flat
const flat = arr.flat(Infinity);Split an array into chunks of a specified size. Use slice and push in a loop for efficient chunking.
- Method: Loop with
arr.slice(i, i + size) - Reduce:
arr.reduce((acc, x, i) => i % size === 0 ? [...acc, [x]] : acc.map((chunk, j) => j === acc.length - 1 ? [...chunk, x] : chunk), []) - Use Cases: Pagination, batch processing
- Edge Cases: Empty array, size larger than array
// Chunk array
function chunkArray(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}
console.log(chunkArray([1, 2, 3, 4, 5, 6], 2)); // [[1,2], [3,4], [5,6]]Binary search finds an element in a sorted array by repeatedly dividing the search interval in half. O(log n) time complexity.
- Method:
binarySearch(arr, target) - Algorithm: While left <= right, check middle
- Recursive: Recursively search left or right half
- Requirement: Array must be sorted
// Binary search
function binarySearch(arr, target) {
let left = 0, right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
console.log(binarySearch([1, 2, 3, 4, 5, 6, 7], 5)); // 4Quick sort is a divide-and-conquer algorithm that picks a pivot and partitions the array around it. O(n log n) average time.
- Algorithm: Choose pivot, partition, recursively sort
- Pivot: First element, last element, random
- In-place: Can be implemented in-place
- Time Complexity: O(n log n) average, O(n²) worst
// Quick sort
function quickSort(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[0];
const left = arr.slice(1).filter(x => x < pivot);
const right = arr.slice(1).filter(x => x >= pivot);
return [...quickSort(left), pivot, ...quickSort(right)];
}
console.log(quickSort([5, 3, 8, 4, 2, 7, 1, 6]));Merge sort is a divide-and-conquer algorithm that divides the array into halves, recursively sorts them, and merges the results. O(n log n) time.
- Algorithm: Divide, recursively sort, merge
- Merge Function: Merge two sorted arrays
- Stable: Maintains relative order of equal elements
- Time Complexity: O(n log n) guaranteed
// Merge sort
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left, right) {
const result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) result.push(left[i++]);
else result.push(right[j++]);
}
return [...result, ...left.slice(i), ...right.slice(j)];
}Bubble sort repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. O(n²) time.
- Algorithm: Compare adjacent, swap if needed
- Optimization: Stop if no swaps in a pass
- Time Complexity: O(n²) worst case
- Use Cases: Educational, small datasets
// Bubble sort
function bubbleSort(arr) {
const sorted = [...arr];
for (let i = 0; i < sorted.length - 1; i++) {
for (let j = 0; j < sorted.length - 1 - i; j++) {
if (sorted[j] > sorted[j + 1]) {
[sorted[j], sorted[j + 1]] = [sorted[j + 1], sorted[j]];
}
}
}
return sorted;
}Find the intersection of two arrays (elements present in both). Use filter with includes or Set for efficiency.
- Filter:
arr1.filter(x => arr2.includes(x)) - Set:
[...new Set(arr1)].filter(x => new Set(arr2).has(x)) - Time Complexity: O(n²) with includes, O(n) with Set
- Unique: Handle duplicates
// Intersection of arrays
function intersection(arr1, arr2) {
return arr1.filter(x => arr2.includes(x));
}
console.log(intersection([1, 2, 3, 4], [3, 4, 5, 6])); // [3, 4]Find the union of two arrays (all elements from both, no duplicates). Use Set for efficient unique union.
- Set:
[...new Set([...arr1, ...arr2])] - Concat:
arr1.concat(arr2.filter(x => !arr1.includes(x))) - Time Complexity: O(n) with Set
- Order: Maintains first occurrence order
// Union of arrays
function union(arr1, arr2) {
return [...new Set([...arr1, ...arr2])];
}
console.log(union([1, 2, 3], [3, 4, 5])); // [1, 2, 3, 4, 5]Find the difference of two arrays (elements in the first array not in the second). Use filter with includes or Set.
- Filter:
arr1.filter(x => !arr2.includes(x)) - Set:
arr1.filter(x => !new Set(arr2).has(x)) - Symmetric Difference:
[...diff1, ...diff2] - Time Complexity: O(n²) with includes, O(n) with Set
// Difference of arrays
function difference(arr1, arr2) {
return arr1.filter(x => !arr2.includes(x));
}
console.log(difference([1, 2, 3, 4], [3, 4, 5, 6])); // [1, 2]Group an array of objects by a property using reduce. Create a new object with groups as keys.
- Method:
arr.reduce((acc, item) => { acc[item.key] = [...(acc[item.key] || []), item]; return acc; }, {}) - Use Cases: Data aggregation, categorization
- Group By Multiple: Use composite keys
- Performance: O(n) time complexity
// Group by property
function groupBy(arr, key) {
return arr.reduce((acc, item) => {
const group = item[key];
if (!acc[group]) acc[group] = [];
acc[group].push(item);
return acc;
}, {});
}
const data = [{type: 'fruit', name: 'apple'}, {type: 'fruit', name: 'banana'}, {type: 'veg', name: 'carrot'}];
console.log(groupBy(data, 'type'));Deep clone an object to create a completely independent copy. Use JSON methods or recursive cloning for complex objects.
- JSON:
JSON.parse(JSON.stringify(obj)) - Recursive: Handle nested objects and arrays
- Limitations: Functions, Date, RegExp, circular references
- Lodash:
_.cloneDeep(obj)
// Deep clone object
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
const original = { a: 1, b: { c: 2 } };
const cloned = deepClone(original);
cloned.b.c = 3;
console.log(original.b.c); // 2
// With circular reference handling
function deepCloneAdvanced(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return seen.get(obj);
const clone = Array.isArray(obj) ? [] : {};
seen.set(obj, clone);
for (const key in obj) {
clone[key] = deepCloneAdvanced(obj[key], seen);
}
return clone;
}Perform immutable updates on nested objects by creating new copies at each level. Use spread operator or libraries like Immer.
- Spread:
{ ...obj, nested: { ...obj.nested, prop: newValue } } - Path: Update by path string
- Immer:
produce(state, draft => { draft.nested.prop = newValue }) - Use Cases: State management, React state
// Immutable update
function updateImmutable(obj, path, value) {
const keys = path.split('.');
if (keys.length === 1) {
return { ...obj, [keys[0]]: value };
}
const [first, ...rest] = keys;
return {
...obj,
[first]: updateImmutable(obj[first] || {}, rest.join('.'), value)
};
}
const state = { user: { name: 'Alice', age: 25 } };
const newState = updateImmutable(state, 'user.age', 26);
console.log(state.user.age); // 25
console.log(newState.user.age); // 26Pipe is a function composition technique that passes the result of one function to the next. It reads left to right.
- Method:
pipe(fn1, fn2, fn3)(value) - Implementation:
fns.reduce((acc, fn) => fn(acc), value) - Use Cases: Data transformation, functional programming
- Reverse: Compose for right-to-left
// Pipe function
function pipe(...fns) {
return (value) => fns.reduce((acc, fn) => fn(acc), value);
}
const double = x => x * 2;
const addTen = x => x + 10;
const square = x => x * x;
const process = pipe(double, addTen, square);
console.log(process(5)); // (5*2+10)^2 = 400Compose is a function composition technique that passes the result of one function to the next, but reads from right to left.
- Method:
compose(fn3, fn2, fn1)(value) - Implementation:
fns.reduceRight((acc, fn) => fn(acc), value) - Use Cases: Data transformation, functional programming
- Order: Functions are applied from right to left
// Compose function
function compose(...fns) {
return (value) => fns.reduceRight((acc, fn) => fn(acc), value);
}
const process2 = compose(square, addTen, double);
console.log(process2(5)); // (5*2+10)^2 = 400Memoization is an optimization technique that caches function results based on arguments to avoid expensive recalculations.
- Method: Cache results in a Map or object
- Key: Serialize arguments as key
- Use Cases: Expensive functions, recursive algorithms
- Trade-off: Memory for speed
// Memoization
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const factorialMemo = memoize(function(n) {
if (n <= 1) return 1;
return n * factorialMemo(n - 1);
});
console.log(factorialMemo(5)); // 120The once function ensures a function is called only once, regardless of how many times it's invoked. Subsequent calls return the cached result.
- Method: Track if function has been called
- Implementation: Use a closure with a flag
- Use Cases: Initialization, setup operations
- Thread Safety: Not needed in single-threaded JavaScript
// Once function
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
const initialize = once(() => {
console.log("Initialized");
return { id: 1, name: "App" };
});
initialize(); // Prints "Initialized"
initialize(); // Returns cached resultDebounce with leading edge executes the function immediately on the first call, then waits for the delay period before allowing another execution.
- Method: Track last call time
- Implementation: Immediate execution, then cooldown
- Use Cases: Save actions, API calls
- Difference: Leading vs trailing edge
// Debounce with leading edge
function debounceLeading(func, delay) {
let timeoutId;
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall < delay) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
lastCall = Date.now();
func.apply(this, args);
}, delay);
} else {
lastCall = now;
func.apply(this, args);
}
};
}Throttle with leading edge executes the function immediately on the first call, then at most once per specified time period.
- Method: Track last call time
- Implementation: Immediate execution, then limit
- Use Cases: Scroll events, resize events
- Difference: Leading vs trailing edge
// Throttle with leading edge
function throttleLeading(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
}Deep equal checks if two values are deeply equal by recursively comparing nested objects and arrays.
- Method: Recursive comparison
- Base Cases: Primitive values, null, undefined
- Objects: Compare keys and values recursively
- Circular References: Handle with Set
// Deep equal
function deepEqual(obj1, obj2) {
if (obj1 === obj2) return true;
if (typeof obj1 !== 'object' || typeof obj2 !== 'object' ||
obj1 === null || obj2 === null) return false;
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (!keys2.includes(key)) return false;
if (!deepEqual(obj1[key], obj2[key])) return false;
}
return true;
}The Observable pattern allows objects to subscribe to changes and get notified when the observable state changes. It's a popular pattern in reactive programming.
- Observable: Maintains a list of subscribers
- Subscriber: Receives notifications
- Methods: subscribe, notify, unsubscribe
- Use Cases: Event handling, state management
// Observable pattern
class Observable {
constructor() {
this.subscribers = [];
}
subscribe(callback) {
this.subscribers.push(callback);
return () => {
this.subscribers = this.subscribers.filter(cb => cb !== callback);
};
}
notify(data) {
this.subscribers.forEach(cb => cb(data));
}
}
const observable = new Observable();
const unsubscribe = observable.subscribe(data => console.log('Received:', data));
observable.notify('Hello'); // Received: Hello
unsubscribe();
observable.notify('World'); // Nothing happensThe Singleton pattern ensures that a class has only one instance and provides a global point of access to it. Useful for configuration, logging, and caching.
- Implementation: Store instance in static property
- Lazy Initialization: Create instance only when needed
- Module Pattern: ES modules are singletons
- Use Cases: Logger, database connection, config
// Singleton pattern
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
this.data = {};
return this;
}
set(key, value) {
this.data[key] = value;
}
get(key) {
return this.data[key];
}
}
const s1 = new Singleton();
const s2 = new Singleton();
s1.set('name', 'Alice');
console.log(s2.get('name')); // Alice
console.log(s1 === s2); // trueThe Factory pattern provides a way to create objects without specifying the exact class. It encapsulates object creation logic and provides flexibility.
- Method: Factory function or class
- Benefits: Decouples creation from usage
- Use Cases: Creating different types of objects
- Parameterized: Pass parameters for customization
// Factory pattern
class UserFactory {
createUser(type, name) {
switch(type) {
case 'admin':
return new Admin(name);
case 'guest':
return new Guest(name);
default:
return new User(name);
}
}
}
class User {
constructor(name) { this.name = name; this.type = 'user'; }
}
class Admin extends User {
constructor(name) { super(name); this.type = 'admin'; }
}
class Guest extends User {
constructor(name) { super(name); this.type = 'guest'; }
}
const factory = new UserFactory();
const admin = factory.createUser('admin', 'Alice');
console.log(admin.type); // adminThe Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the algorithm to vary independently.
- Context: Uses a strategy object
- Strategy: Interface for algorithms
- Benefits: Open/closed principle, runtime switching
- Use Cases: Payment methods, sorting algorithms
// Strategy pattern
class PaymentStrategy {
pay(amount) { throw new Error('Must implement pay'); }
}
class CreditCardStrategy extends PaymentStrategy {
pay(amount) {
console.log(`Paid ${amount} with Credit Card`);
}
}
class PayPalStrategy extends PaymentStrategy {
pay(amount) {
console.log(`Paid ${amount} with PayPal`);
}
}
class CryptoStrategy extends PaymentStrategy {
pay(amount) {
console.log(`Paid ${amount} with Crypto`);
}
}
class PaymentContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
executePayment(amount) {
this.strategy.pay(amount);
}
}
const context = new PaymentContext(new CreditCardStrategy());
context.executePayment(100);
context.setStrategy(new PayPalStrategy());
context.executePayment(50);The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically.
- Subject: Maintains observers
- Observer: Receives updates
- Benefits: Loose coupling, event-driven architecture
- Use Cases: Event handling, pub/sub systems
// Observer pattern
class Subject {
constructor() {
this.observers = [];
}
attach(observer) {
this.observers.push(observer);
}
detach(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class ConcreteObserver {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} received: ${data}`);
}
}
const subject = new Subject();
const observer1 = new ConcreteObserver('Observer1');
const observer2 = new ConcreteObserver('Observer2');
subject.attach(observer1);
subject.attach(observer2);
subject.notify('Hello World');The Decorator pattern allows behavior to be added to individual objects dynamically without affecting other objects from the same class.
- Component: Base interface
- Decorator: Wraps component and adds behavior
- Benefits: Flexible extension, open/closed principle
- Use Cases: Logging, authentication, caching
// Decorator pattern
class Coffee {
cost() { return 5; }
description() { return 'Coffee'; }
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 2; }
description() { return this.coffee.description() + ', Milk'; }
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 1; }
description() { return this.coffee.description() + ', Sugar'; }
}
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.description()); // Coffee, Milk, Sugar
console.log(coffee.cost()); // 8The Command pattern encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing, logging, and undo operations.
- Command: Encapsulates request
- Invoker: Executes commands
- Receiver: Performs the actual work
- Benefits: Undo/redo, queuing, logging
// Command pattern
class Command {
execute() {}
undo() {}
}
class AddCommand extends Command {
constructor(receiver, value) {
super();
this.receiver = receiver;
this.value = value;
}
execute() {
this.receiver.add(this.value);
}
undo() {
this.receiver.subtract(this.value);
}
}
class Calculator {
constructor() {
this.value = 0;
}
add(n) { this.value += n; }
subtract(n) { this.value -= n; }
getValue() { return this.value; }
}
const calc = new Calculator();
const addCommand = new AddCommand(calc, 5);
addCommand.execute();
console.log(calc.getValue()); // 5
addCommand.undo();
console.log(calc.getValue()); // 0The Memento pattern captures and externalizes an object's internal state so that the object can be restored to that state later without violating encapsulation.
- Originator: Creates and restores mementos
- Memento: Stores internal state
- Caretaker: Manages mementos
- Benefits: State restoration, undo/redo
// Memento pattern
class Memento {
constructor(state) {
this.state = state;
}
getState() { return this.state; }
}
class Originator {
constructor() {
this.state = '';
}
setState(state) {
this.state = state;
}
getState() {
return this.state;
}
saveState() {
return new Memento(this.state);
}
restoreState(memento) {
this.state = memento.getState();
}
}
class Caretaker {
constructor() {
this.mementos = [];
}
addMemento(memento) {
this.mementos.push(memento);
}
getMemento(index) {
return this.mementos[index];
}
}The Mediator pattern defines an object that encapsulates how a set of objects interact. It promotes loose coupling by keeping objects from referring to each other explicitly.
- Mediator: Encapsulates communication
- Colleague: Communicates through mediator
- Benefits: Loose coupling, centralized control
- Use Cases: Chat systems, UI components
// Mediator pattern
class Mediator {
constructor() {
this.colleagues = [];
}
register(colleague) {
this.colleagues.push(colleague);
}
send(message, sender) {
this.colleagues.forEach(colleague => {
if (colleague !== sender) {
colleague.receive(message);
}
});
}
}
class Colleague {
constructor(mediator, name) {
this.mediator = mediator;
this.name = name;
this.mediator.register(this);
}
send(message) {
this.mediator.send(message, this);
}
receive(message) {
console.log(`${this.name} received: ${message}`);
}
}
const mediator = new Mediator();
const alice = new Colleague(mediator, 'Alice');
const bob = new Colleague(mediator, 'Bob');
alice.send('Hello Bob!');The Chain of Responsibility pattern passes a request along a chain of handlers until one of them handles it. Each handler decides whether to process the request or pass it on.
- Handler: Processes or forwards request
- Chain: Linked list of handlers
- Benefits: Decoupling, dynamic configuration
- Use Cases: Logging, authentication, middleware
// Chain of Responsibility
class Handler {
constructor() {
this.next = null;
}
setNext(handler) {
this.next = handler;
return handler;
}
handle(request) {
if (this.next) {
this.next.handle(request);
}
}
}
class AuthHandler extends Handler {
handle(request) {
if (request.token) {
console.log('Authentication passed');
super.handle(request);
} else {
console.log('Authentication failed');
}
}
}
class LoggerHandler extends Handler {
handle(request) {
console.log(`Logging request: ${request.url}`);
super.handle(request);
}
}
const auth = new AuthHandler();
const logger = new LoggerHandler();
auth.setNext(logger);
auth.handle({ token: 'valid', url: '/api' });The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
- Context: Maintains state
- State: Defines behavior for each state
- Benefits: Clean state management, avoids conditionals
- Use Cases: State machines, workflow engines
// State pattern
class State {
handle() {}
}
class ReadyState extends State {
handle() {
console.log('Ready: Waiting for input');
}
}
class ProcessingState extends State {
handle() {
console.log('Processing: Working on task');
}
}
class CompletedState extends State {
handle() {
console.log('Completed: Task finished');
}
}
class Context {
constructor() {
this.state = new ReadyState();
}
setState(state) {
this.state = state;
}
request() {
this.state.handle();
}
}
const context2 = new Context();
context2.request(); // Ready: Waiting for input
context2.setState(new ProcessingState());
context2.request(); // Processing: Working on task
context2.setState(new CompletedState());
context2.request(); // Completed: Task finishedThe Proxy pattern provides a surrogate or placeholder for another object to control access to it. It can add behavior like caching, logging, or access control.
- Subject: Real object
- Proxy: Controls access to subject
- Benefits: Access control, lazy loading, logging
- Use Cases: Virtual proxies, protection proxies
// Proxy pattern
class RealSubject {
request() {
console.log('RealSubject: Handling request');
}
}
class Proxy {
constructor(realSubject) {
this.realSubject = realSubject;
}
request() {
if (this.checkAccess()) {
this.realSubject.request();
this.logAccess();
}
}
checkAccess() {
console.log('Proxy: Checking access');
return true;
}
logAccess() {
console.log('Proxy: Logging access');
}
}
const real = new RealSubject();
const proxy = new Proxy(real);
proxy.request();The Flyweight pattern minimizes memory usage by sharing as much data as possible with similar objects. It's useful for large numbers of similar objects.
- Flyweight: Shared object
- Factory: Manages flyweights
- Benefits: Memory optimization, performance
- Use Cases: Text rendering, caching
// Flyweight pattern
class Flyweight {
constructor(sharedState) {
this.sharedState = sharedState;
}
operation(uniqueState) {
console.log(`Shared: ${this.sharedState}, Unique: ${uniqueState}`);
}
}
class FlyweightFactory {
constructor() {
this.flyweights = {};
}
getFlyweight(sharedState) {
if (!this.flyweights[sharedState]) {
this.flyweights[sharedState] = new Flyweight(sharedState);
}
return this.flyweights[sharedState];
}
}The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently. It's useful for separating interface from implementation.
- Abstraction: High-level interface
- Implementation: Low-level operations
- Benefits: Separation of concerns, flexibility
- Use Cases: Cross-platform applications
// Bridge pattern
class Abstraction {
constructor(implementation) {
this.implementation = implementation;
}
operation() {
this.implementation.operation();
}
}
class RefinedAbstraction extends Abstraction {
operation() {
console.log('RefinedAbstraction: Additional logic');
this.implementation.operation();
}
}
class ConcreteImplementationA {
operation() {
console.log('ConcreteImplementationA: Operation');
}
}
class ConcreteImplementationB {
operation() {
console.log('ConcreteImplementationB: Operation');
}
}The Adapter pattern converts the interface of a class into another interface that clients expect. It allows incompatible interfaces to work together.
- Target: Expected interface
- Adaptee: Existing interface
- Adapter: Bridges target and adaptee
- Benefits: Reusability, legacy integration
// Adapter pattern
class Target {
request() {
console.log('Target: Request');
}
}
class Adaptee {
specificRequest() {
console.log('Adaptee: Specific Request');
}
}
class Adapter extends Target {
constructor(adaptee) {
super();
this.adaptee = adaptee;
}
request() {
this.adaptee.specificRequest();
}
}
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
adapter.request();The Facade pattern provides a simplified interface to a complex subsystem. It hides the complexity and makes the subsystem easier to use.
- Facade: Simplified interface
- Subsystem: Complex components
- Benefits: Simplified interface, decoupling
- Use Cases: Library APIs, complex systems
// Facade pattern
class SubsystemA {
operationA() { console.log('SubsystemA: Operation'); }
}
class SubsystemB {
operationB() { console.log('SubsystemB: Operation'); }
}
class Facade {
constructor() {
this.subsystemA = new SubsystemA();
this.subsystemB = new SubsystemB();
}
operation() {
this.subsystemA.operationA();
this.subsystemB.operationB();
console.log('Facade: Complex operation');
}
}
const facade = new Facade();
facade.operation();The Composite pattern composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions uniformly.
- Component: Interface for all objects
- Leaf: Individual object
- Composite: Container of components
- Benefits: Uniform interface, tree structures
// Composite pattern
class Component {
operation() {}
}
class Leaf extends Component {
operation() {
console.log('Leaf: Operation');
}
}
class Composite extends Component {
constructor() {
super();
this.children = [];
}
add(component) {
this.children.push(component);
}
remove(component) {
this.children = this.children.filter(c => c !== component);
}
operation() {
console.log('Composite: Operation');
this.children.forEach(child => child.operation());
}
}The Visitor pattern lets you add further operations to objects without having to modify them. It separates an algorithm from the object structure.
- Visitor: Defines operations
- Element: Accepts visitors
- Benefits: Adding new operations without modifying elements
- Use Cases: Compilers, AST traversal
// Visitor pattern
class Visitor {
visitElementA(element) {}
visitElementB(element) {}
}
class Element {
accept(visitor) {}
}
class ElementA extends Element {
accept(visitor) {
visitor.visitElementA(this);
}
}
class ElementB extends Element {
accept(visitor) {
visitor.visitElementB(this);
}
}
class ConcreteVisitor extends Visitor {
visitElementA(element) {
console.log('Visiting ElementA');
}
visitElementB(element) {
console.log('Visiting ElementB');
}
}The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
- Iterator: Traverses collection
- Aggregate: Creates iterator
- Benefits: Uniform traversal, multiple iterators
- Use Cases: Collection traversal, custom data structures
// Iterator pattern
class Iterator {
constructor(collection) {
this.collection = collection;
this.index = 0;
}
next() {
return this.collection[this.index++];
}
hasNext() {
return this.index < this.collection.length;
}
}
class CustomCollection {
constructor() {
this.items = [];
}
add(item) {
this.items.push(item);
}
getIterator() {
return new Iterator(this.items);
}
}
const collection = new CustomCollection();
collection.add('A');
collection.add('B');
collection.add('C');
const iterator = collection.getIterator();
while (iterator.hasNext()) {
console.log(iterator.next());
}The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. It lets subclasses redefine certain steps without changing the algorithm's structure.
- AbstractClass: Defines template method
- ConcreteClass: Implements abstract steps
- Benefits: Code reuse, consistent algorithm structure
- Use Cases: Frameworks, algorithms with customizable steps
// Template Method pattern
class AbstractClass {
templateMethod() {
this.step1();
this.step2();
this.step3();
}
step1() { console.log('Step 1'); }
step2() { console.log('Step 2'); }
step3() { console.log('Step 3'); }
}
class ConcreteClass extends AbstractClass {
step2() {
console.log('Concrete Step 2');
}
}
const concrete = new ConcreteClass();
concrete.templateMethod();The Builder pattern constructs complex objects step by step. It separates the construction of a complex object from its representation.
- Builder: Constructs parts
- Director: Orchestrates construction
- Product: The constructed object
- Benefits: Step-by-step construction, reusable builder
// Builder pattern
class Product {
constructor() {
this.parts = [];
}
add(part) { this.parts.push(part); }
listParts() { console.log(this.parts.join(', ')); }
}
class Builder {
reset() {}
buildStepA() {}
buildStepB() {}
getResult() {}
}
class ConcreteBuilder extends Builder {
constructor() {
super();
this.product = new Product();
}
reset() {
this.product = new Product();
}
buildStepA() {
this.product.add('Part A');
}
buildStepB() {
this.product.add('Part B');
}
getResult() {
return this.product;
}
}
class Director {
constructor(builder) {
this.builder = builder;
}
buildMinimal() {
this.builder.buildStepA();
}
buildFull() {
this.builder.buildStepA();
this.builder.buildStepB();
}
}The Prototype pattern creates new objects by cloning an existing object, rather than instantiating new ones. It's useful when creating objects is expensive.
- Prototype: Cloneable object
- Clone: Creates a copy
- Benefits: Performance, avoids constructors
- Use Cases: Complex objects, expensive creation
// Prototype pattern
class Prototype {
clone() {
return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
}
deepClone() {
return JSON.parse(JSON.stringify(this));
}
}
class ConcretePrototype extends Prototype {
constructor(name) {
super();
this.name = name;
this.nested = { value: 42 };
}
}
const original2 = new ConcretePrototype('Original');
const copy = original2.clone();
copy.name = 'Copy';
copy.nested.value = 99;
console.log(original2.name); // Original
console.log(original2.nested.value); // 99 (shallow copy)
const deepCopy = original2.deepClone();
deepCopy.nested.value = 100;
console.log(original2.nested.value); // 99 (deep copy)