- Variables: var, let, const
- Scope: global, function, block
- Hoisting
- Closures
- Execution Context
- Call Stack
- Event Loop
- this keyword
- Prototype and Prototype Chain
- Inheritance
- Functions: declaration, expression, arrow
- Higher Order Functions
- Call, Apply, Bind
- Currying
- Debouncing and Throttling
- Promises
- Async/Await
- Callback vs Promise vs Async/Await
- Microtasks vs Macrotasks
- Shallow Copy vs Deep Copy
- Pass by Value vs Reference
- Type Coercion
- == vs ===
- null vs undefined
- map, filter, reduce
- forEach, find, some, every
- Destructuring
- Spread and Rest Operators
- Optional Chaining
- Modules: CommonJS vs ES Modules
- Generators and Iterators
- Symbols
- Proxy and Reflect
- WeakMap and WeakSet
- DOM Manipulation
- Event Bubbling and Capturing
- Event Delegation
- preventDefault() vs stopPropagation()
- LocalStorage vs SessionStorage vs Cookies
- IndexedDB basics
- Browser Rendering Process
- Reflow vs Repaint
- Critical Rendering Path
- Web Workers
- Service Workers
- Caching basics
- Intersection Observer
- Mutation Observer
- Semantic HTML
- Forms and Validation
- Accessibility basics (a11y)
- SEO basics
- Meta tags
- defer vs async
- Script loading behavior
- Data Attributes
- Box Model
- Specificity
- Positioning
- Flexbox
- Grid
- Responsive Design
- Media Queries
- Units: px, %, rem, em, vh, vw
- CSS Selectors
- Pseudo Classes and Elements
- z-index
- BFC basics
- Transitions vs Animations
- CSS Variables
- CSS Preprocessors
- Virtual DOM
- JSX
- Components
- Props vs State
- Functional vs Class Components
- Hooks (useState, useEffect, useMemo, useCallback, useRef)
- Custom Hooks
- Component Lifecycle
- Controlled vs Uncontrolled Components
- Lifting State Up
- Context API
- React.memo
- Re-rendering
- Key prop
- Reconciliation
- Lazy Loading
- Code Splitting
- Error Boundaries
- Suspense
- Performance Optimization
- Portals
- Refs and forwardRef
- SSR vs CSR vs SSG vs ISR
- Routing
- Dynamic Routes
- API Routes
- Middleware basics
- Image Optimization
- SEO in Next.js
- App Router vs Pages Router
- Pagination
- Infinite Scroll
- Search Suggestions
- Debounced Search
- Caching Strategies
- Retry Mechanism
- Rate Limiting basics
- Optimistic UI
- Offline Support
- Large List Rendering
- Virtualization
- State Management basics
- REST APIs
- HTTP Methods
- Status Codes
- Authentication vs Authorization
- JWT basics
- CORS
- Cookies vs Tokens
- Fetch vs Axios
- Polling vs WebSockets
- GraphQL basics
- Request Interceptors
Theory:
var: function-scoped, hoisted, can be redeclaredlet: block-scoped, hoisted but not initialized, cannot be redeclaredconst: block-scoped, hoisted but not initialized, cannot be reassigned
// var - function scoped
function varExample() {
var x = 1;
if (true) {
var x = 2; // same variable
console.log(x); // 2
}
console.log(x); // 2
}
// let - block scoped
function letExample() {
let x = 1;
if (true) {
let x = 2; // different variable
console.log(x); // 2
}
console.log(x); // 1
}
// const - cannot reassign
const obj = { name: 'John' };
// obj = {}; // Error
obj.name = 'Jane'; // OK - mutating object is allowed
// Temporal Dead Zone
console.log(a); // undefined (var hoisting)
// console.log(b); // ReferenceError (TDZ)
var a = 1;
let b = 2;Theory:
- Global scope: accessible everywhere
- Function scope: accessible within function
- Block scope: accessible within
{}
// Global scope
var globalVar = 'global';
function scopeExample() {
// Function scope
var functionVar = 'function';
if (true) {
// Block scope
let blockVar = 'block';
const blockConst = 'block const';
var functionVar2 = 'function2'; // function scoped!
console.log(globalVar); // 'global'
console.log(functionVar); // 'function'
console.log(blockVar); // 'block'
}
console.log(functionVar2); // 'function2'
// console.log(blockVar); // ReferenceError
}
// Lexical scoping
function outer() {
const outerVar = 'outer';
function inner() {
console.log(outerVar); // 'outer' - lexical scope
}
inner();
}Theory: Variable and function declarations are moved to the top of their scope during compilation.
// Function hoisting
greet(); // 'Hello' - works!
function greet() {
console.log('Hello');
}
// Variable hoisting
console.log(x); // undefined (not ReferenceError)
var x = 5;
console.log(x); // 5
// Equivalent to:
var x;
console.log(x); // undefined
x = 5;
// let/const hoisting (TDZ)
// console.log(y); // ReferenceError
let y = 10;
// Function expression not hoisted
// sayHi(); // TypeError
var sayHi = function() {
console.log('Hi');
};
// Class hoisting
// const p = new Person(); // ReferenceError
class Person {
constructor(name) {
this.name = name;
}
}Theory: A closure is a function that has access to variables in its outer (enclosing) lexical scope, even after the outer function has returned.
// Basic closure
function outer() {
const message = 'Hello';
function inner() {
console.log(message); // accesses outer variable
}
return inner;
}
const closureFn = outer();
closureFn(); // 'Hello' - still has access!
// Practical example: Private variables
function createCounter() {
let count = 0; // private variable
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// console.log(counter.count); // undefined - private!
// Common mistake: Loop closure
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 (var is function-scoped)
// Fix with let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 (let is block-scoped)
// Fix with IIFE
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Prints: 0, 1, 2Theory:
Environment where JavaScript code is executed. Contains variable environment, scope chain, and this binding.
// Global Execution Context
var globalVar = 'global';
function executionExample() {
// Function Execution Context
var localVar = 'local';
console.log(globalVar); // 'global'
console.log(localVar); // 'local'
}
// Execution Context phases:
// 1. Creation Phase:
// - Create scope chain
// - Create variable object (hoisting)
// - Determine 'this'
// 2. Execution Phase:
// - Assign values
// - Execute code
function contextPhases() {
console.log(x); // undefined (creation phase)
var x = 10;
console.log(x); // 10 (execution phase)
}Theory: LIFO data structure that tracks function execution. Each function call creates a new execution context pushed onto the stack.
function first() {
console.log('First');
second();
console.log('First again');
}
function second() {
console.log('Second');
third();
console.log('Second again');
}
function third() {
console.log('Third');
}
first();
// Call Stack visualization:
// 1. first() pushed
// 2. second() pushed
// 3. third() pushed
// 4. third() popped (prints 'Third')
// 5. second() popped (prints 'Second again')
// 6. first() popped (prints 'First again')
// Stack overflow
function recursive() {
recursive(); // infinite recursion
}
// recursive(); // RangeError: Maximum call stack size exceededTheory: Mechanism that handles asynchronous operations. Continuously checks call stack and task queues.
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// Output: 1, 4, 3, 2
// Explanation:
// 1. Synchronous code executes first (1, 4)
// 2. Microtasks (Promises) execute (3)
// 3. Macrotasks (setTimeout) execute (2)
// Event Loop phases:
// 1. Execute synchronous code
// 2. Execute all microtasks
// 3. Execute one macrotask
// 4. Repeat
// Complex example
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise in Timeout 1'));
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
setTimeout(() => console.log('Timeout in Promise 1'), 0);
})
.then(() => console.log('Promise 2'));
console.log('End');
// Output: Start, End, Promise 1, Promise 2, Timeout 1, Promise in Timeout 1, Timeout in Promise 1Theory:
this refers to the object that is executing the current function. Its value depends on how the function is called.
// Global context
console.log(this); // Window (browser) or global (Node.js)
// Object method
const obj = {
name: 'John',
greet() {
console.log(this.name);
}
};
obj.greet(); // 'John'
// Function call
function showThis() {
console.log(this);
}
showThis(); // Window (non-strict) or undefined (strict mode)
// Arrow functions (lexical this)
const obj2 = {
name: 'Jane',
greet: () => {
console.log(this.name); // undefined - arrow function doesn't have own 'this'
},
greet2() {
const inner = () => {
console.log(this.name); // 'Jane' - inherits from greet2
};
inner();
}
};
// Constructor function
function Person(name) {
this.name = name;
}
const person = new Person('Alice');
console.log(person.name); // 'Alice'
// Event handler
document.getElementById('btn')?.addEventListener('click', function() {
console.log(this); // button element
});
// Explicit binding
const obj3 = { name: 'Bob' };
function sayName() {
console.log(this.name);
}
sayName.call(obj3); // 'Bob'
sayName.apply(obj3); // 'Bob'
const boundFn = sayName.bind(obj3);
boundFn(); // 'Bob'Theory:
Every object has an internal [[Prototype]] property that references another object. Used for inheritance.
// Prototype basics
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};
const john = new Person('John');
john.greet(); // 'Hello, I'm John'
console.log(john.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
// Prototype chain
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null (end of chain)
// Checking prototype chain
console.log(john instanceof Person); // true
console.log(john instanceof Object); // true
console.log(Person.prototype.isPrototypeOf(john)); // true
// Adding to prototype
Array.prototype.last = function() {
return this[this.length - 1];
};
console.log([1, 2, 3].last()); // 3
// Object.create
const parent = {
greet() {
console.log('Hello from parent');
}
};
const child = Object.create(parent);
child.greet(); // 'Hello from parent'
console.log(child.__proto__ === parent); // trueTheory: Mechanism where one object can inherit properties and methods from another.
// Prototypal inheritance
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound`);
};
function Dog(name, breed) {
Animal.call(this, name); // call parent constructor
this.breed = breed;
}
// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} barks`);
};
const dog = new Dog('Rex', 'Labrador');
dog.speak(); // 'Rex makes a sound'
dog.bark(); // 'Rex barks'
// ES6 Class inheritance
class AnimalES6 {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class DogES6 extends AnimalES6 {
constructor(name, breed) {
super(name); // call parent constructor
this.breed = breed;
}
bark() {
console.log(`${this.name} barks`);
}
}
const dogES6 = new DogES6('Max', 'Golden Retriever');
dogES6.speak(); // 'Max makes a sound'
dogES6.bark(); // 'Max barks'
// Multiple inheritance (mixin pattern)
const canEat = {
eat() {
console.log('Eating');
}
};
const canWalk = {
walk() {
console.log('Walking');
}
};
class Human {
constructor(name) {
this.name = name;
}
}
Object.assign(Human.prototype, canEat, canWalk);
const human = new Human('Alice');
human.eat(); // 'Eating'
human.walk(); // 'Walking'Theory: Different ways to define functions with varying behaviors.
// Function Declaration
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5
// Function Expression
const subtract = function(a, b) {
return a - b;
};
console.log(subtract(5, 2)); // 3
// Arrow Function
const multiply = (a, b) => a * b;
console.log(multiply(3, 4)); // 12
// Arrow function variations
const single = x => x * 2; // single param, no parens
const noParams = () => console.log('Hi');
const multiLine = (a, b) => {
const sum = a + b;
return sum * 2;
};
// Differences:
// 1. 'this' binding
const obj = {
name: 'Test',
regular: function() {
console.log(this.name); // 'Test'
},
arrow: () => {
console.log(this.name); // undefined (lexical this)
}
};
// 2. arguments object
function regularFunc() {
console.log(arguments); // [1, 2, 3]
}
regularFunc(1, 2, 3);
const arrowFunc = () => {
// console.log(arguments); // ReferenceError
};
// Use rest parameters instead
const arrowWithRest = (...args) => {
console.log(args); // [1, 2, 3]
};
arrowWithRest(1, 2, 3);
// 3. Constructor
function RegularConstructor() {
this.value = 42;
}
const instance = new RegularConstructor(); // OK
const ArrowConstructor = () => {
this.value = 42;
};
// const instance2 = new ArrowConstructor(); // TypeError
// Named function expression
const factorial = function fact(n) {
return n <= 1 ? 1 : n * fact(n - 1);
};
console.log(factorial(5)); // 120Theory: Functions that take functions as arguments or return functions.
// Function as argument
function higherOrder(callback) {
callback();
}
higherOrder(() => console.log('Callback executed'));
// Function returning function
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Common HOFs
const numbers = [1, 2, 3, 4, 5];
// map
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]
// reduce
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15
// Composing functions
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const addOne = x => x + 1;
const multiplyByTwo = x => x * 2;
const square = x => x * x;
const composed = compose(square, multiplyByTwo, addOne);
console.log(composed(3)); // ((3 + 1) * 2)^2 = 64
// Pipe (left to right)
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
const piped = pipe(addOne, multiplyByTwo, square);
console.log(piped(3)); // ((3 + 1) * 2)^2 = 64Theory:
Methods to explicitly set this context and pass arguments.
const person = {
name: 'John',
greet(greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}
};
const anotherPerson = { name: 'Jane' };
// call - arguments passed individually
person.greet.call(anotherPerson, 'Hello', '!');
// 'Hello, I'm Jane!'
// apply - arguments passed as array
person.greet.apply(anotherPerson, ['Hi', '.']);
// 'Hi, I'm Jane.'
// bind - returns new function with bound context
const boundGreet = person.greet.bind(anotherPerson);
boundGreet('Hey', '!!!');
// 'Hey, I'm Jane!!!'
// Partial application with bind
const greetJane = person.greet.bind(anotherPerson, 'Hello');
greetJane('!'); // 'Hello, I'm Jane!'
// Practical example: borrowing methods
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers);
console.log(max); // 7
// Modern alternative with spread
console.log(Math.max(...numbers)); // 7
// Fixing 'this' in callbacks
const obj = {
count: 0,
increment() {
this.count++;
}
};
// Problem
// setTimeout(obj.increment, 100); // 'this' is lost
// Solution 1: bind
setTimeout(obj.increment.bind(obj), 100);
// Solution 2: arrow function
setTimeout(() => obj.increment(), 100);
// Solution 3: call
function executeCallback(callback, context) {
callback.call(context);
}
executeCallback(obj.increment, obj);Theory: Transforming a function with multiple arguments into a sequence of functions each taking a single argument.
// Regular function
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
// Curried version
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
console.log(curriedAdd(1)(2)(3)); // 6
// Arrow function currying
const curriedAddArrow = a => b => c => a + b + c;
console.log(curriedAddArrow(1)(2)(3)); // 6
// Generic curry function
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
const curriedSum = curry((a, b, c) => a + b + c);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
// Practical example
const multiply = a => b => a * b;
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Currying with map
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.map(double)); // [2, 4, 6, 8, 10]
// Partial application
const greet = greeting => name => `${greeting}, ${name}!`;
const sayHello = greet('Hello');
const sayHi = greet('Hi');
console.log(sayHello('John')); // 'Hello, John!'
console.log(sayHi('Jane')); // 'Hi, Jane!'Theory: Techniques to limit function execution frequency.
// Debouncing - execute after delay, reset timer on each call
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage
const searchAPI = query => {
console.log('Searching for:', query);
};
const debouncedSearch = debounce(searchAPI, 300);
// Only last call executes after 300ms
debouncedSearch('a');
debouncedSearch('ab');
debouncedSearch('abc'); // Only this executes
// Throttling - execute at most once per interval
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage
const handleScroll = () => {
console.log('Scroll event');
};
const throttledScroll = throttle(handleScroll, 1000);
// Executes immediately, then at most once per second
window.addEventListener('scroll', throttledScroll);
// Advanced debounce with immediate option
function debounceAdvanced(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);
}
};
}
// Practical React example
const SearchComponent = () => {
const [query, setQuery] = useState('');
const searchAPI = useCallback(
debounce(q => {
fetch(`/api/search?q=${q}`);
}, 300),
[]
);
const handleChange = e => {
setQuery(e.target.value);
searchAPI(e.target.value);
};
return <input value={query} onChange={handleChange} />;
};Theory: Object representing eventual completion or failure of an asynchronous operation.
// Creating a promise
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('Operation successful');
} else {
reject('Operation failed');
}
});
// Consuming promises
promise
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => console.log('Cleanup'));
// Promise states: pending, fulfilled, rejected
// Chaining promises
fetch('/api/user')
.then(response => response.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(response => response.json())
.then(posts => console.log(posts))
.catch(error => console.error(error));
// Promise.all - wait for all promises
const promise1 = Promise.resolve(3);
const promise2 = Promise.resolve(42);
const promise3 = new Promise(resolve => setTimeout(() => resolve('foo'), 100));
Promise.all([promise1, promise2, promise3])
.then(values => console.log(values)); // [3, 42, 'foo']
// Promise.race - first to settle
Promise.race([
new Promise(resolve => setTimeout(() => resolve('slow'), 500)),
new Promise(resolve => setTimeout(() => resolve('fast'), 100))
]).then(result => console.log(result)); // 'fast'
// Promise.allSettled - wait for all, regardless of outcome
Promise.allSettled([
Promise.resolve(1),
Promise.reject('error'),
Promise.resolve(3)
]).then(results => console.log(results));
// [{status: 'fulfilled', value: 1}, {status: 'rejected', reason: 'error'}, {status: 'fulfilled', value: 3}]
// Promise.any - first fulfilled promise
Promise.any([
Promise.reject('error1'),
Promise.resolve('success'),
Promise.resolve('success2')
]).then(result => console.log(result)); // 'success'
// Creating resolved/rejected promises
const resolved = Promise.resolve('value');
const rejected = Promise.reject('error');
// Promisifying callback-based functions
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) reject(error);
else resolve(result);
});
});
};
}Theory: Syntactic sugar over promises, making asynchronous code look synchronous.
// Basic async/await
async function fetchUser() {
const response = await fetch('/api/user');
const user = await response.json();
return user;
}
// Error handling
async function fetchUserWithError() {
try {
const response = await fetch('/api/user');
if (!response.ok) throw new Error('Failed to fetch');
const user = await response.json();
return user;
} catch (error) {
console.error(error);
throw error;
}
}
// Async functions always return a promise
async function getValue() {
return 42;
}
getValue().then(value => console.log(value)); // 42
// Awaiting multiple promises
async function fetchMultiple() {
// Sequential (slow)
const user = await fetch('/api/user').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
// Parallel (fast)
const [user2, posts2] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
]);
return { user2, posts2 };
}
// Top-level await (ES2022)
// const data = await fetch('/api/data');
// Async arrow functions
const asyncArrow = async () => {
const result = await someAsyncOperation();
return result;
};
// Async IIFE
(async () => {
const data = await fetchData();
console.log(data);
})();
// Error handling patterns
async function handleErrors() {
// Pattern 1: try/catch
try {
const data = await fetchData();
return data;
} catch (error) {
console.error(error);
}
// Pattern 2: catch on promise
const data2 = await fetchData().catch(error => {
console.error(error);
return null;
});
return data2;
}
// Async iteration
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
(async () => {
for await (const value of asyncGenerator()) {
console.log(value); // 1, 2, 3
}
})();Theory: Evolution of asynchronous programming patterns in JavaScript.
// 1. Callback pattern
function fetchDataCallback(callback) {
setTimeout(() => {
callback(null, { data: 'result' });
}, 1000);
}
fetchDataCallback((error, result) => {
if (error) {
console.error(error);
} else {
console.log(result);
}
});
// Callback hell
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
console.log(d);
});
});
});
});
// 2. Promise pattern
function fetchDataPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ data: 'result' });
}, 1000);
});
}
fetchDataPromise()
.then(result => console.log(result))
.catch(error => console.error(error));
// Promise chaining (solves callback hell)
getData()
.then(a => getMoreData(a))
.then(b => getMoreData(b))
.then(c => getMoreData(c))
.then(d => console.log(d))
.catch(error => console.error(error));
// 3. Async/Await pattern
async function fetchDataAsync() {
try {
const a = await getData();
const b = await getMoreData(a);
const c = await getMoreData(b);
const d = await getMoreData(c);
console.log(d);
} catch (error) {
console.error(error);
}
}
// Comparison
// Callbacks: Hard to read, callback hell, error handling complex
// Promises: Better readability, chainable, standardized error handling
// Async/Await: Most readable, synchronous-looking code, easy error handling
// Converting between patterns
// Callback to Promise
function callbackToPromise(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) reject(error);
else resolve(result);
});
});
};
}
// Promise to Async/Await (just use await)
async function usePromise() {
const result = await somePromise();
return result;
}Theory: Different queues for asynchronous operations with different priorities.
// Macrotasks: setTimeout, setInterval, setImmediate, I/O
// Microtasks: Promise.then, queueMicrotask, MutationObserver
console.log('1'); // Synchronous
setTimeout(() => {
console.log('2'); // Macrotask
}, 0);
Promise.resolve().then(() => {
console.log('3'); // Microtask
});
queueMicrotask(() => {
console.log('4'); // Microtask
});
console.log('5'); // Synchronous
// Output: 1, 5, 3, 4, 2
// Execution order:
// 1. All synchronous code
// 2. All microtasks
// 3. One macrotask
// 4. All microtasks created by that macrotask
// 5. Repeat from step 3
// Complex example
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise in Timeout 1'));
}, 0);
setTimeout(() => {
console.log('Timeout 2');
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
setTimeout(() => console.log('Timeout in Promise 1'), 0);
})
.then(() => console.log('Promise 2'));
console.log('End');
// Output:
// Start
// End
// Promise 1
// Promise 2
// Timeout 1
// Promise in Timeout 1
// Timeout 2
// Timeout in Promise 1
// Practical implications
async function processData() {
console.log('Processing');
// This runs in next microtask
await Promise.resolve();
console.log('After await');
// This runs in next macrotask
setTimeout(() => console.log('In timeout'), 0);
}
processData();
console.log('After function call');
// Output:
// Processing
// After function call
// After await
// In timeoutTheory: Shallow copy copies references, deep copy creates new objects recursively.
// Shallow copy methods
const original = {
name: 'John',
address: { city: 'NYC' }
};
// 1. Spread operator
const shallow1 = { ...original };
shallow1.name = 'Jane'; // doesn't affect original
shallow1.address.city = 'LA'; // AFFECTS original!
// 2. Object.assign
const shallow2 = Object.assign({}, original);
// 3. Array shallow copy
const arr = [1, 2, [3, 4]];
const shallowArr1 = [...arr];
const shallowArr2 = arr.slice();
const shallowArr3 = Array.from(arr);
shallowArr1[2][0] = 99; // affects original!
// Deep copy methods
// 1. JSON.parse/stringify (limitations: no functions, dates, undefined, symbols)
const deep1 = JSON.parse(JSON.stringify(original));
deep1.address.city = 'Boston'; // doesn't affect original
// Limitations
const objWithFunction = {
name: 'John',
greet() { console.log('Hi'); },
date: new Date(),
undef: undefined,
sym: Symbol('test')
};
const copied = JSON.parse(JSON.stringify(objWithFunction));
// Lost: greet function, date becomes string, undefined removed, symbol removed
// 2. structuredClone (modern, handles more types)
const deep2 = structuredClone(original);
deep2.address.city = 'Chicago'; // doesn't affect original
// 3. Custom deep clone
function deepClone(obj, hash = new WeakMap()) {
// Handle primitives and null
if (obj === null || typeof obj !== 'object') return obj;
// Handle circular references
if (hash.has(obj)) return hash.get(obj);
// Handle Date
if (obj instanceof Date) return new Date(obj);
// Handle Array
if (Array.isArray(obj)) {
const arrCopy = [];
hash.set(obj, arrCopy);
obj.forEach((item, index) => {
arrCopy[index] = deepClone(item, hash);
});
return arrCopy;
}
// Handle Object
const objCopy = {};
hash.set(obj, objCopy);
Object.keys(obj).forEach(key => {
objCopy[key] = deepClone(obj[key], hash);
});
return objCopy;
}
// Circular reference example
const circular = { name: 'Test' };
circular.self = circular;
const cloned = deepClone(circular);
console.log(cloned.self === cloned); // true
// Comparison
const test = {
a: 1,
b: { c: 2 }
};
const shallow = { ...test };
const deep = deepClone(test);
test.b.c = 99;
console.log(shallow.b.c); // 99 (affected)
console.log(deep.b.c); // 2 (not affected)Theory: Primitives are passed by value, objects are passed by reference.
// Primitives (pass by value)
let a = 10;
let b = a; // copy of value
b = 20;
console.log(a); // 10 (unchanged)
console.log(b); // 20
function modifyPrimitive(x) {
x = 100;
}
let num = 50;
modifyPrimitive(num);
console.log(num); // 50 (unchanged)
// Objects (pass by reference)
let obj1 = { value: 10 };
let obj2 = obj1; // reference to same object
obj2.value = 20;
console.log(obj1.value); // 20 (changed!)
console.log(obj2.value); // 20
function modifyObject(obj) {
obj.value = 100; // modifies original
}
let myObj = { value: 50 };
modifyObject(myObj);
console.log(myObj.value); // 100 (changed!)
// Reassignment vs mutation
function reassignObject(obj) {
obj = { value: 200 }; // creates new object, doesn't affect original
}
let myObj2 = { value: 50 };
reassignObject(myObj2);
console.log(myObj2.value); // 50 (unchanged)
// Arrays (also pass by reference)
let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2.push(4);
console.log(arr1); // [1, 2, 3, 4] (changed!)
function modifyArray(arr) {
arr.push(5); // modifies original
}
modifyArray(arr1);
console.log(arr1); // [1, 2, 3, 4, 5]
function reassignArray(arr) {
arr = [10, 20]; // doesn't affect original
}
reassignArray(arr1);
console.log(arr1); // [1, 2, 3, 4, 5] (unchanged)
// Avoiding mutations
function safeModify(obj) {
const copy = { ...obj };
copy.value = 100;
return copy;
}
let original = { value: 50 };
let modified = safeModify(original);
console.log(original.value); // 50 (unchanged)
console.log(modified.value); // 100
// Comparison
let x = { a: 1 };
let y = { a: 1 };
console.log(x === y); // false (different references)
let z = x;
console.log(x === z); // true (same reference)Theory: Automatic or implicit conversion of values from one type to another.
// String coercion
console.log('5' + 3); // '53' (number to string)
console.log('5' + true); // '5true'
console.log('5' + null); // '5null'
console.log('5' + undefined); // '5undefined'
// Number coercion
console.log('5' - 3); // 2 (string to number)
console.log('5' * '2'); // 10
console.log('5' / '2'); // 2.5
console.log('5' % '2'); // 1
// Boolean coercion
console.log(Boolean('')); // false
console.log(Boolean('hello')); // true
console.log(Boolean(0)); // false
console.log(Boolean(42)); // true
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean([])); // true
console.log(Boolean({})); // true
// Falsy values: false, 0, '', null, undefined, NaN
// Everything else is truthy
// Explicit coercion
console.log(String(123)); // '123'
console.log(Number('123')); // 123
console.log(Number('123abc')); // NaN
console.log(parseInt('123abc')); // 123
console.log(parseFloat('123.45abc')); // 123.45
// Implicit coercion in conditions
if ('hello') console.log('truthy'); // executes
if (0) console.log('truthy'); // doesn't execute
// Coercion with operators
console.log(true + true); // 2
console.log(true + false); // 1
console.log([] + []); // '' (empty string)
console.log([] + {}); // '[object Object]'
console.log({} + []); // '[object Object]' or 0 (depends on context)
// Unary plus operator
console.log(+'42'); // 42
console.log(+true); // 1
console.log(+false); // 0
console.log(+null); // 0
console.log(+undefined); // NaN
// Double NOT operator (converts to boolean)
console.log(!!'hello'); // true
console.log(!!0); // false
// Common pitfalls
console.log(null == undefined); // true (coercion)
console.log(null === undefined); // false (no coercion)
console.log('0' == 0); // true (coercion)
console.log('0' === 0); // false (no coercion)
console.log(false == '0'); // true
console.log(false === '0'); // falseTheory:
== performs type coercion, === checks type and value without coercion.
// === (strict equality)
console.log(5 === 5); // true
console.log(5 === '5'); // false (different types)
console.log(true === 1); // false
console.log(null === undefined); // false
console.log(NaN === NaN); // false (special case)
// == (loose equality with coercion)
console.log(5 == 5); // true
console.log(5 == '5'); // true (string coerced to number)
console.log(true == 1); // true
console.log(false == 0); // true
console.log(null == undefined); // true (special case)
console.log('' == 0); // true
console.log('0' == 0); // true
console.log(false == '0'); // true
// Comparison rules for ==
// 1. If types are same, compare like ===
// 2. null == undefined (and vice versa)
// 3. Number compared with string: convert string to number
// 4. Boolean: convert to number (true -> 1, false -> 0)
// 5. Object compared with primitive: convert object to primitive
// Object comparison
const obj1 = { a: 1 };
const obj2 = { a: 1 };
const obj3 = obj1;
console.log(obj1 == obj2); // false (different references)
console.log(obj1 === obj2); // false
console.log(obj1 == obj3); // true (same reference)
console.log(obj1 === obj3); // true
// Array comparison
console.log([] == []); // false (different references)
console.log([] == false); // true (coercion)
console.log([] == 0); // true
console.log([''] == ''); // true
// Best practices
// Always use === unless you specifically need coercion
// Use == only for null/undefined checks
if (value == null) {
// checks for both null and undefined
}
// Equivalent to:
if (value === null || value === undefined) {
// more explicit
}
// Special cases
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log(+0 === -0); // true
console.log(Object.is(+0, -0)); // false
// Object.is() is more precise than ===Theory:
null is intentional absence of value, undefined is uninitialized or missing value.
// undefined - variable declared but not assigned
let a;
console.log(a); // undefined
console.log(typeof a); // 'undefined'
// null - intentional absence of value
let b = null;
console.log(b); // null
console.log(typeof b); // 'object' (historical bug)
// When you get undefined
let x;
console.log(x); // undefined (uninitialized)
function test(param) {
console.log(param); // undefined (missing argument)
}
test();
const obj = { name: 'John' };
console.log(obj.age); // undefined (missing property)
function noReturn() {}
console.log(noReturn()); // undefined (no return value)
// When you get null
const element = document.getElementById('nonexistent');
console.log(element); // null (not found)
const match = 'hello'.match(/\d+/);
console.log(match); // null (no match)
// Comparison
console.log(null == undefined); // true (loose equality)
console.log(null === undefined); // false (strict equality)
console.log(null == 0); // false
console.log(undefined == 0); // false
// Checking for null or undefined
function checkValue(value) {
// Check for both
if (value == null) {
console.log('null or undefined');
}
// Check specifically
if (value === null) {
console.log('null');
}
if (value === undefined) {
console.log('undefined');
}
// Check for falsy (includes null, undefined, 0, '', false)
if (!value) {
console.log('falsy');
}
}
// Default values
function greet(name) {
// Old way
name = name || 'Guest';
// Better way (handles 0, '', false correctly)
name = name ?? 'Guest'; // nullish coalescing
console.log(`Hello, ${name}`);
}
// Default parameters
function greet2(name = 'Guest') {
console.log(`Hello, ${name}`);
}
// Optional chaining
const user = {
name: 'John',
address: null
};
console.log(user.address?.city); // undefined (no error)
console.log(user.contact?.phone); // undefined
// Setting to null vs deleting
const obj2 = { a: 1, b: 2 };
obj2.a = null; // property exists with null value
delete obj2.b; // property removed
console.log('a' in obj2); // true
console.log('b' in obj2); // falseTheory: Array methods for transforming, filtering, and aggregating data.
const numbers = [1, 2, 3, 4, 5];
// map - transform each element
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
const names = users.map(user => user.name);
console.log(names); // ['John', 'Jane']
// map with index
const withIndex = numbers.map((n, index) => `${index}: ${n}`);
console.log(withIndex); // ['0: 1', '1: 2', '2: 3', '3: 4', '4: 5']
// filter - keep elements that pass test
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]
const adults = [
{ name: 'John', age: 25 },
{ name: 'Jane', age: 17 },
{ name: 'Bob', age: 30 }
].filter(person => person.age >= 18);
console.log(adults); // [{ name: 'John', age: 25 }, { name: 'Bob', age: 30 }]
// reduce - aggregate to single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 15
const product = numbers.reduce((acc, n) => acc * n, 1);
console.log(product); // 120
// reduce for complex transformations
const items = [
{ category: 'fruit', name: 'apple' },
{ category: 'fruit', name: 'banana' },
{ category: 'vegetable', name: 'carrot' }
];
const grouped = items.reduce((acc, item) => {
if (!acc[item.category]) {
acc[item.category] = [];
}
acc[item.category].push(item.name);
return acc;
}, {});
console.log(grouped);
// { fruit: ['apple', 'banana'], vegetable: ['carrot'] }
// Chaining methods
const result = numbers
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((acc, n) => acc + n, 0);
console.log(result); // 12 (2*2 + 4*2 = 4 + 8)
// Practical example: counting occurrences
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const count = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(count); // { apple: 3, banana: 2, orange: 1 }
// Flattening arrays
const nested = [[1, 2], [3, 4], [5]];
const flattened = nested.reduce((acc, arr) => acc.concat(arr), []);
console.log(flattened); // [1, 2, 3, 4, 5]
// Modern alternative: flat()
console.log(nested.flat()); // [1, 2, 3, 4, 5]
// Implementing map with reduce
const customMap = (arr, fn) => {
return arr.reduce((acc, item, index) => {
acc.push(fn(item, index));
return acc;
}, []);
};
// Implementing filter with reduce
const customFilter = (arr, fn) => {
return arr.reduce((acc, item, index) => {
if (fn(item, index)) {
acc.push(item);
}
return acc;
}, []);
};Theory: Array iteration and search methods.
const numbers = [1, 2, 3, 4, 5];
// forEach - iterate over each element (no return value)
numbers.forEach((n, index) => {
console.log(`Index ${index}: ${n}`);
});
// Cannot break out of forEach
numbers.forEach(n => {
if (n === 3) return; // only skips current iteration
console.log(n);
});
// find - returns first element that matches
const found = numbers.find(n => n > 3);
console.log(found); // 4
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
{ id: 3, name: 'Bob' }
];
const user = users.find(u => u.id === 2);
console.log(user); // { id: 2, name: 'Jane' }
// Returns undefined if not found
const notFound = numbers.find(n => n > 10);
console.log(notFound); // undefined
// findIndex - returns index of first match
const index = numbers.findIndex(n => n > 3);
console.log(index); // 3
// some - returns true if ANY element passes test
const hasEven = numbers.some(n => n % 2 === 0);
console.log(hasEven); // true
const hasNegative = numbers.some(n => n < 0);
console.log(hasNegative); // false
// every - returns true if ALL elements pass test
const allPositive = numbers.every(n => n > 0);
console.log(allPositive); // true
const allEven = numbers.every(n => n % 2 === 0);
console.log(allEven); // false
// Practical examples
const products = [
{ name: 'Laptop', price: 1000, inStock: true },
{ name: 'Phone', price: 500, inStock: false },
{ name: 'Tablet', price: 300, inStock: true }
];
// Check if any product is out of stock
const hasOutOfStock = products.some(p => !p.inStock);
console.log(hasOutOfStock); // true
// Check if all products are in stock
const allInStock = products.every(p => p.inStock);
console.log(allInStock); // false
// Find expensive product
const expensive = products.find(p => p.price > 800);
console.log(expensive); // { name: 'Laptop', ... }
// forEach vs map
// forEach: side effects, no return value
const result1 = numbers.forEach(n => n * 2);
console.log(result1); // undefined
// map: returns new array
const result2 = numbers.map(n => n * 2);
console.log(result2); // [2, 4, 6, 8, 10]
// Performance: some/every short-circuit
const largeArray = Array.from({ length: 1000000 }, (_, i) => i);
// Stops at first match
const hasFive = largeArray.some(n => n === 5); // Fast
// Checks all elements
const allChecked = largeArray.forEach(n => n === 5); // SlowerTheory: Extract values from arrays or properties from objects into distinct variables.
// Array destructuring
const arr = [1, 2, 3, 4, 5];
const [first, second] = arr;
console.log(first, second); // 1, 2
// Skip elements
const [a, , c] = arr;
console.log(a, c); // 1, 3
// Rest operator
const [head, ...tail] = arr;
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
// Default values
const [x, y, z = 0] = [1, 2];
console.log(z); // 0
// Swapping variables
let p = 1, q = 2;
[p, q] = [q, p];
console.log(p, q); // 2, 1
// Object destructuring
const user = {
name: 'John',
age: 30,
email: 'john@example.com'
};
const { name, age } = user;
console.log(name, age); // 'John', 30
// Rename variables
const { name: userName, age: userAge } = user;
console.log(userName, userAge); // 'John', 30
// Default values
const { name: n, country = 'USA' } = user;
console.log(country); // 'USA'
// Nested destructuring
const person = {
name: 'Jane',
address: {
city: 'NYC',
zip: '10001'
}
};
const { address: { city, zip } } = person;
console.log(city, zip); // 'NYC', '10001'
// Rest in objects
const { name: personName, ...rest } = user;
console.log(rest); // { age: 30, email: 'john@example.com' }
// Function parameters
function greet({ name, age }) {
console.log(`Hello ${name}, you are ${age}`);
}
greet(user); // 'Hello John, you are 30'
// With defaults
function greet2({ name = 'Guest', age = 0 } = {}) {
console.log(`Hello ${name}, you are ${age}`);
}
greet2(); // 'Hello Guest, you are 0'
// Array of objects
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
users.forEach(({ id, name }) => {
console.log(`${id}: ${name}`);
});
// Computed property names
const key = 'name';
const { [key]: value } = user;
console.log(value); // 'John'
// Practical React example
function UserProfile({ user: { name, email, avatar } }) {
return (
<div>
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}Theory:
Spread (...) expands iterables, Rest (...) collects multiple elements.
// Spread operator
// Arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// Copy array
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3] (unchanged)
// Objects
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 2, c: 3, d: 4 }
// Override properties
const defaults = { theme: 'light', lang: 'en' };
const userPrefs = { theme: 'dark' };
const settings = { ...defaults, ...userPrefs };
console.log(settings); // { theme: 'dark', lang: 'en' }
// Function arguments
const numbers = [1, 2, 3];
console.log(Math.max(...numbers)); // 3
// String to array
const str = 'hello';
const chars = [...str];
console.log(chars); // ['h', 'e', 'l', 'l', 'o']
// Rest operator
// Function parameters
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// Must be last parameter
function greet(greeting, ...names) {
return `${greeting} ${names.join(', ')}`;
}
console.log(greet('Hello', 'John', 'Jane', 'Bob'));
// 'Hello John, Jane, Bob'
// Array destructuring
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [2, 3, 4, 5]
// Object destructuring
const user = { name: 'John', age: 30, email: 'john@example.com' };
const { name, ...otherProps } = user;
console.log(name); // 'John'
console.log(otherProps); // { age: 30, email: 'john@example.com' }
// Practical examples
// Immutable array operations
const todos = ['Task 1', 'Task 2', 'Task 3'];
// Add item
const withNew = [...todos, 'Task 4'];
// Remove item
const withoutSecond = [...todos.slice(0, 1), ...todos.slice(2)];
// Update item
const updated = todos.map((todo, i) =>
i === 1 ? 'Updated Task' : todo
);
// Immutable object updates
const state = {
user: { name: 'John', age: 30 },
theme: 'light'
};
// Update nested property
const newState = {
...state,
user: {
...state.user,
age: 31
}
};
// React props spreading
function Button(props) {
return <button {...props}>Click me</button>;
}
// Usage
<Button className="btn" onClick={handleClick} disabled={false} />
// Conditional spreading
const conditionalProps = {
...(isDisabled && { disabled: true }),
...(hasError && { className: 'error' })
};Theory: Safely access nested object properties without checking each level.
// Without optional chaining
const user = {
name: 'John',
address: {
city: 'NYC'
}
};
// Verbose null checking
const city = user && user.address && user.address.city;
console.log(city); // 'NYC'
// With optional chaining
const city2 = user?.address?.city;
console.log(city2); // 'NYC'
// Returns undefined if any part is null/undefined
const zip = user?.address?.zip;
console.log(zip); // undefined (no error!)
// Accessing nested properties
const user2 = null;
console.log(user2?.address?.city); // undefined (no error)
// Optional chaining with arrays
const users = [
{ name: 'John', address: { city: 'NYC' } },
{ name: 'Jane' }
];
console.log(users[0]?.address?.city); // 'NYC'
console.log(users[1]?.address?.city); // undefined
console.log(users[2]?.address?.city); // undefined
// Optional chaining with function calls
const obj = {
method() {
return 'result';
}
};
console.log(obj.method?.()); // 'result'
console.log(obj.nonExistent?.()); // undefined (no error)
// Dynamic property access
const propName = 'address';
console.log(user?.[propName]?.city); // 'NYC'
// Combining with nullish coalescing
const displayCity = user?.address?.city ?? 'Unknown';
console.log(displayCity); // 'NYC'
const displayZip = user?.address?.zip ?? 'N/A';
console.log(displayZip); // 'N/A'
// Practical examples
// API response handling
const response = {
data: {
user: {
profile: {
avatar: 'url'
}
}
}
};
const avatar = response?.data?.user?.profile?.avatar ?? 'default.png';
// Event handling
document.getElementById('btn')?.addEventListener('click', () => {
console.log('Clicked');
});
// React component
function UserProfile({ user }) {
return (
<div>
<h2>{user?.name ?? 'Guest'}</h2>
<p>{user?.email ?? 'No email'}</p>
<img src={user?.avatar ?? 'default.png'} />
</div>
);
}
// Array methods
const items = null;
const firstItem = items?.[0];
const length = items?.length ?? 0;
const mapped = items?.map(x => x * 2) ?? [];
// Short-circuit evaluation
let count = 0;
const result = null?.method(count++);
console.log(count); // 0 (method not called)
// Cannot use on left side of assignment
// user?.address?.city = 'LA'; // SyntaxError
// Comparison with other operators
const value = null;
// Optional chaining
console.log(value?.prop); // undefined
// Logical AND
console.log(value && value.prop); // null
// Difference: optional chaining only checks null/undefined
// Logical AND checks for falsy values (0, '', false, etc.)
const obj2 = { count: 0 };
console.log(obj2?.count); // 0
console.log(obj2 && obj2.count); // 0
console.log(obj2.count ?? 'default'); // 0
console.log(obj2.count || 'default'); // 'default'Theory: Different module systems for organizing and sharing code.
// CommonJS (Node.js traditional)
// Exporting
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
// Or
exports.add = add;
exports.subtract = subtract;
// Single export
module.exports = function multiply(a, b) {
return a * b;
};
// Importing
const math = require('./math');
console.log(math.add(2, 3)); // 5
// Destructuring
const { add, subtract } = require('./math');
console.log(add(2, 3)); // 5
// ES Modules (modern JavaScript)
// Exporting
// math.mjs or math.js (with "type": "module" in package.json)
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// Default export
export default function multiply(a, b) {
return a * b;
}
// Named + default export
export { add, subtract };
export default multiply;
// Importing
import multiply from './math.js';
import { add, subtract } from './math.js';
import multiply, { add, subtract } from './math.js';
// Rename imports
import { add as addition } from './math.js';
// Import all
import * as math from './math.js';
console.log(math.add(2, 3));
// Import for side effects only
import './setup.js';
// Dynamic imports
const math = await import('./math.js');
// Or
import('./math.js').then(math => {
console.log(math.add(2, 3));
});
// Key differences
// 1. Syntax
// CommonJS: require/module.exports
// ES Modules: import/export
// 2. Loading
// CommonJS: synchronous, runtime
// ES Modules: asynchronous, compile-time
// 3. Tree shaking
// CommonJS: not supported
// ES Modules: supported (removes unused code)
// 4. Top-level await
// CommonJS: not supported
// ES Modules: supported
// 5. this context
// CommonJS: this === module.exports
// ES Modules: this === undefined
// 6. File extension
// CommonJS: .js, .cjs
// ES Modules: .mjs, .js (with package.json config)
// Interoperability
// Import CommonJS in ES Module
import pkg from './commonjs-module.js';
const { method } = pkg;
// Import ES Module in CommonJS (async)
(async () => {
const { method } = await import('./es-module.mjs');
})();
// Practical examples
// utils.js (ES Module)
export const API_URL = 'https://api.example.com';
export function fetchData(endpoint) {
return fetch(`${API_URL}/${endpoint}`);
}
export class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
get(endpoint) {
return fetch(`${this.baseURL}/${endpoint}`);
}
}
export default new ApiClient(API_URL);
// app.js
import apiClient, { fetchData, API_URL } from './utils.js';
// Conditional imports
let module;
if (condition) {
module = await import('./module-a.js');
} else {
module = await import('./module-b.js');
}
// Code splitting in React
const LazyComponent = React.lazy(() => import('./Component.js'));Theory: Generators are functions that can pause and resume execution. Iterators define custom iteration behavior.
// Generator function
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
// Using in for...of
for (const num of numberGenerator()) {
console.log(num); // 1, 2, 3
}
// Infinite generator
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const infinite = infiniteSequence();
console.log(infinite.next().value); // 0
console.log(infinite.next().value); // 1
console.log(infinite.next().value); // 2
// Generator with parameters
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]
// Passing values to generator
function* twoWayGenerator() {
const a = yield 'First';
console.log('Received:', a);
const b = yield 'Second';
console.log('Received:', b);
}
const gen2 = twoWayGenerator();
console.log(gen2.next()); // { value: 'First', done: false }
console.log(gen2.next('Value A')); // Logs: Received: Value A
// { value: 'Second', done: false }
console.log(gen2.next('Value B')); // Logs: Received: Value B
// { value: undefined, done: true }
// Delegating generators
function* gen1() {
yield 1;
yield 2;
}
function* gen2() {
yield* gen1();
yield 3;
yield 4;
}
console.log([...gen2()]); // [1, 2, 3, 4]
// Iterator protocol
const iterableObject = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
const data = this.data;
return {
next() {
if (index < data.length) {
return { value: data[index++], done: false };
}
return { done: true };
}
};
}
};
for (const value of iterableObject) {
console.log(value); // 1, 2, 3
}
// Practical examples
// Pagination
function* paginate(items, pageSize) {
for (let i = 0; i < items.length; i += pageSize) {
yield items.slice(i, i + pageSize);
}
}
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9];
for (const page of paginate(items, 3)) {
console.log(page); // [1,2,3], [4,5,6], [7,8,9]
}
// ID generator
function* idGenerator() {
let id = 1;
while (true) {
yield id++;
}
}
const getId = idGenerator();
console.log(getId.next().value); // 1
console.log(getId.next().value); // 2
// Fibonacci sequence
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
const fib = fibonacci();
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
console.log(fib.next().value); // 5
// Async generator
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
(async () => {
for await (const value of asyncGenerator()) {
console.log(value); // 1, 2, 3
}
})();
// Tree traversal
function* traverseTree(node) {
yield node.value;
if (node.left) yield* traverseTree(node.left);
if (node.right) yield* traverseTree(node.right);
}
const tree = {
value: 1,
left: { value: 2 },
right: { value: 3 }
};
console.log([...traverseTree(tree)]); // [1, 2, 3]Theory: Unique and immutable primitive values, often used as object property keys.
// Creating symbols
const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // false (always unique)
// With description
const sym3 = Symbol('description');
console.log(sym3.toString()); // 'Symbol(description)'
// Symbol registry (shared symbols)
const globalSym1 = Symbol.for('app.id');
const globalSym2 = Symbol.for('app.id');
console.log(globalSym1 === globalSym2); // true
// Get key from symbol
console.log(Symbol.keyFor(globalSym1)); // 'app.id'
console.log(Symbol.keyFor(sym1)); // undefined (not in registry)
// As object properties
const id = Symbol('id');
const user = {
name: 'John',
[id]: 123
};
console.log(user[id]); // 123
console.log(user.id); // undefined
// Symbols are hidden from normal iteration
console.log(Object.keys(user)); // ['name']
console.log(Object.getOwnPropertyNames(user)); // ['name']
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]
// Well-known symbols
// Symbol.iterator
const iterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => ({
value: this.data[index++],
done: index > this.data.length
})
};
}
};
console.log([...iterable]); // [1, 2, 3]
// Symbol.toStringTag
class MyClass {
get [Symbol.toStringTag]() {
return 'MyClass';
}
}
console.log(Object.prototype.toString.call(new MyClass()));
// '[object MyClass]'
// Symbol.hasInstance
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true
// Symbol.toPrimitive
const obj = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') return 42;
if (hint === 'string') return 'hello';
return true;
}
};
console.log(+obj); // 42
console.log(`${obj}`); // 'hello'
console.log(obj + ''); // 'true'
// Practical use cases
// Private properties (before # syntax)
const _private = Symbol('private');
class MyClass2 {
constructor() {
this[_private] = 'secret';
}
getPrivate() {
return this[_private];
}
}
const instance = new MyClass2();
console.log(instance.getPrivate()); // 'secret'
console.log(instance[_private]); // undefined (symbol not accessible)
// Enum-like values
const Colors = {
RED: Symbol('red'),
GREEN: Symbol('green'),
BLUE: Symbol('blue')
};
function getColorName(color) {
switch (color) {
case Colors.RED: return 'Red';
case Colors.GREEN: return 'Green';
case Colors.BLUE: return 'Blue';
}
}
// Metadata
const metadata = Symbol('metadata');
class User {
constructor(name) {
this.name = name;
this[metadata] = {
createdAt: new Date(),
version: 1
};
}
}
// Avoiding property name collisions
const myLib = {
[Symbol.for('myLib.version')]: '1.0.0'
};
const userCode = {
version: '2.0.0', // doesn't conflict
...myLib
};