✗ Variable hoisted but undefined (TDZ for let/const)
Conditional function creation, callbacks, function as data
Named Function Expression
const f = function name(params) { body };
✗ Not hoisted; name only visible inside function
Recursion in expressions, better stack traces
Arrow Function ES6
const name = (params) => expression
✗ Variable hoisting behavior
Short callbacks, lexical this binding, concise syntax
Method Definition ES6
{ methodName(params) { body } }
✗ Part of object/class definition
Object methods, class methods with concise syntax
Generator Function ES6
function* name(params) { yield value }
✓ Hoisted like regular functions
Iterators, lazy evaluation, pausable execution
Async Function ES8
async function name(params) { await promise }
✓ Hoisted (declaration) / ✗ (expression)
Asynchronous operations with cleaner syntax
Example: Function declaration vs expression
// Function Declaration - hoisted, can call before definitionconsole.log(add(2, 3)); // Works: 5function add(a, b) { return a + b;}// Function Expression - not hoisted// console.log(subtract(5, 2)); // Error: Cannot access before initializationconst subtract = function(a, b) { return a - b;};// Named Function Expression - name visible only insideconst factorial = function fact(n) { return n <= 1 ? 1 : n * fact(n - 1); // 'fact' accessible here};console.log(factorial(5)); // 120// console.log(fact(5)); // Error: fact is not defined// Arrow function - concise syntaxconst multiply = (a, b) => a * b;const square = x => x * x; // Single param, no parens neededconst greet = () => 'Hello'; // No params
Best Practice: Use function declarations for top-level utility functions that need hoisting.
Use arrow functions for callbacks and methods that don't need their own this. Use function
expressions when functions are conditional or need to be treated as values.
2. Arrow Functions (=>) and Lexical this Binding
Feature
Arrow Function
Regular Function
Impact
this Binding
Lexical (from enclosing scope)
Dynamic (based on call)
No .bind() needed for callbacks
arguments Object
✗ Not available (use rest params)
✓ Available
Use ...args for arrow functions
new Operator
✗ Cannot be constructor
✓ Can be constructor
Arrow functions have no [[Construct]]
prototype Property
✗ No prototype
✓ Has prototype
Lighter memory footprint for arrows
super Keyword
✗ Cannot use super
✓ Can use in methods
Use regular functions for class methods needing super
yield Keyword
✗ Cannot be generator
✓ Can yield
Use function* for generators
Syntax Form
Example
When to Use
No Parameters
() => expression
Functions with no inputs
Single Parameter
x => expression
Unary functions (map, filter callbacks)
Multiple Parameters
(x, y) => expression
Binary/multi-parameter functions
Expression Body
x => x * 2
Single expression, implicit return
Block Body
x => { return x * 2; }
Multiple statements, explicit return
Object Literal Return
x => ({ value: x })
Returning object (wrap in parentheses)
Example: Lexical this binding in practice
// Problem with regular functions - this binding changesfunction Timer() { this.seconds = 0; // Regular function - 'this' is window/undefined in strict mode setInterval(function() { this.seconds++; // ✗ Wrong 'this' console.log(this.seconds); // NaN }, 1000);}// Solution 1: Arrow function - lexical 'this'function Timer() { this.seconds = 0; setInterval(() => { this.seconds++; // ✓ Correct 'this' from Timer console.log(this.seconds); // 1, 2, 3... }, 1000);}// Solution 2: Old approach with .bind() (before ES6)function Timer() { this.seconds = 0; setInterval(function() { this.seconds++; }.bind(this), 1000); // Explicitly bind 'this'}// Class methods with arrow functionsclass Counter { count = 0; // Arrow function as class field - 'this' always bound to instance increment = () => { this.count++; } // Regular method - 'this' depends on how it's called decrement() { this.count--; }}const counter = new Counter();const inc = counter.increment;const dec = counter.decrement;inc(); // ✓ Works - arrow function has lexical 'this'dec(); // ✗ Error - 'this' is undefined (not bound)
Example: Arrow function syntax variations
// Expression body - implicit returnconst double = x => x * 2;const sum = (a, b) => a + b;// Block body - explicit return neededconst greet = name => { const message = `Hello, ${name}!`; return message.toUpperCase();};// Returning object literal - wrap in parenthesesconst makePerson = (name, age) => ({ name, age });const point = (x, y) => ({ x: x, y: y });// Array methods with arrowsconst numbers = [1, 2, 3, 4, 5];const doubled = numbers.map(n => n * 2);const evens = numbers.filter(n => n % 2 === 0);const total = numbers.reduce((sum, n) => sum + n, 0);// Chaining with concise arrowsconst result = [1, 2, 3, 4, 5] .filter(x => x % 2 === 0) .map(x => x * x) .reduce((sum, x) => sum + x, 0); // 20
Warning: Don't use arrow functions as object methods or event handlers where you need dynamic
this. Arrow functions cannot be used as constructors with new. They don't have
arguments object - use rest parameters instead.
3. Function Parameters (default, rest, destructuring)
Parameter Type
Syntax
Description
Example
Required Parameters
function f(a, b)
Traditional positional parameters; undefined if not provided
f(1, 2)
Default Parameters ES6
function f(a = 0, b = 1)
Provides default value if undefined (not for null)
f() // a=0, b=1
Rest Parameters ES6
function f(...args)
Collects remaining arguments into array; must be last
f(1,2,3) // args=[1,2,3]
Destructuring (Array) ES6
function f([a, b])
Extracts values from array argument by position
f([10, 20])
Destructuring (Object) ES6
function f({x, y})
Extracts properties from object argument by name
f({x:1, y:2})
Nested Destructuring ES6
function f({a: {b}})
Extracts nested properties from deep object structure
f({a: {b: 5}})
Default + Destructuring ES6
function f({x = 0} = {})
Combines destructuring with defaults for object properties
f() // x=0
Pattern
Example
Behavior
Default with Expression
f(a = computeDefault())
Expression evaluated only if parameter is undefined
// Rest parameters collect remaining arguments into arrayfunction sum(...numbers) { return numbers.reduce((total, n) => total + n, 0);}console.log(sum(1, 2, 3, 4)); // 10console.log(sum()); // 0 (empty array)// Combine regular params with restfunction multiply(multiplier, ...numbers) { return numbers.map(n => n * multiplier);}console.log(multiply(2, 1, 2, 3)); // [2, 4, 6]// Rest must be last parameter// function invalid(...args, last) {} // ✗ SyntaxError// Rest vs arguments objectfunction oldWay() { // arguments is array-like, not real array const args = Array.from(arguments); return args.reduce((sum, n) => sum + n, 0);}function newWay(...args) { // args is real array with all array methods return args.reduce((sum, n) => sum + n, 0);}// Arrow functions don't have arguments, use restconst arrowSum = (...nums) => nums.reduce((s, n) => s + n, 0);
Best Practice: Prefer declarative HOFs (map, filter, reduce) over imperative loops for data
transformation. Chain operations for readability. Use arrow functions for concise callbacks. Remember that HOFs
enable code reuse and abstraction.
5. Closures and Function Scope Management
Concept
Definition
Key Characteristics
Closure
Function that retains access to variables from its outer (enclosing) scope even after outer function has
returned
Lexical scoping, persistent state, data privacy
Lexical Environment
Internal structure holding variable bindings and reference to outer environment
Created at function execution, forms scope chain
Scope Chain
Chain of lexical environments searched for variable resolution
Inner to outer traversal, stops at first match
Private Variables
Variables accessible only through closures, not directly from outside
Encapsulation, data hiding, module pattern
Closure Pattern
Use Case
Example Pattern
Function Factory
Create specialized functions with preset parameters
function makeAdder(x) { return y => x + y; }
Private State
Encapsulate data with controlled access
function counter() { let n=0; return ()=>++n; }
Module Pattern
Create modules with public/private members
IIFE returning object with methods accessing private vars
Event Handlers
Preserve context in async callbacks
Handler functions accessing outer scope variables
Memoization
Cache function results using closure-stored cache
Closure over cache object for recursive functions
Example: Basic closure mechanics
// Simple closure - inner function accesses outer variablefunction outer() { let count = 0; // Variable in outer scope function inner() { count++; // Accesses count from outer scope return count; } return inner;}const counter = outer();console.log(counter()); // 1console.log(counter()); // 2console.log(counter()); // 3// 'count' persists between calls through closure// Multiple closures over same variablefunction makeCounter() { let count = 0; return { increment: () => ++count, decrement: () => --count, value: () => count };}const c1 = makeCounter();const c2 = makeCounter(); // Separate closure, separate countc1.increment();c1.increment();console.log(c1.value()); // 2console.log(c2.value()); // 0 (independent)
Example: Practical closure patterns
// Function factory with closurefunction multiplier(factor) { return function(number) { return number * factor; // 'factor' remembered via closure };}const double = multiplier(2);const triple = multiplier(3);console.log(double(5)); // 10console.log(triple(5)); // 15// Private variables patternfunction createBankAccount(initialBalance) { let balance = initialBalance; // Private variable return { deposit(amount) { if (amount > 0) balance += amount; return balance; }, withdraw(amount) { if (amount > 0 && amount <= balance) balance -= amount; return balance; }, getBalance() { return balance; } }; // No direct access to 'balance' from outside}const account = createBankAccount(100);account.deposit(50); // 150account.withdraw(30); // 120console.log(account.balance); // undefined (private)console.log(account.getBalance()); // 120// Memoization with closurefunction memoize(fn) { const cache = {}; // Closure over cache return function(...args) { const key = JSON.stringify(args); if (key in cache) { console.log('Cached'); return cache[key]; } const result = fn(...args); cache[key] = result; return result; };}const slowFunction = (n) => { console.log('Computing...'); return n * 2;};const fast = memoize(slowFunction);fast(5); // Computing... 10fast(5); // Cached 10
Common Pitfall: Closures in loops. Using var creates single binding shared across
iterations. Solution: use let (creates new binding per iteration) or IIFE to capture value.
// ✗ Problem with varfor (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100);}// Logs: 3, 3, 3 (all closures see same 'i')// ✓ Solution 1: Use letfor (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100);}// Logs: 0, 1, 2 (each closure gets own 'i')// ✓ Solution 2: IIFE (old approach)for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 100); })(i);}// Logs: 0, 1, 2
6. IIFE Patterns (Immediately Invoked Function Expression) and Module Creation
Pattern
Syntax
Purpose
Use Case
Basic IIFE
(function() { })();
Execute function immediately, create private scope
Avoid global pollution, one-time initialization
Alternative Syntax
(function() { }());
Same as basic IIFE (different grouping)
Style preference, same behavior
Arrow IIFE ES6
(() => { })();
Concise IIFE with lexical this
Modern alternative, shorter syntax
Named IIFE
(function name() { })();
Self-reference for recursion, better debugging
Recursive IIFEs, stack traces
IIFE with Parameters
(function(x) { })(value);
Pass values into IIFE scope
Dependency injection, value capture
IIFE Returning Value
const x = (function() { return val; })();
Initialize variable with complex logic
Complex initialization, module pattern
Module Pattern (IIFE)
const mod = (function() { return {...}; })();
Create module with public API, private state
Pre-ES6 modules, encapsulation
Example: IIFE basics and variations
// Basic IIFE - executes immediately(function() { const message = 'Hello from IIFE'; console.log(message);})(); // Logs: Hello from IIFE// 'message' not accessible here (private scope)// IIFE with parameters(function(name) { console.log(`Hello, ${name}!`);})('Alice'); // Hello, Alice!// IIFE returning valueconst config = (function() { const privateKey = 'secret123'; return { apiUrl: 'https://api.example.com', timeout: 5000, getKey: () => privateKey // Controlled access };})();console.log(config.apiUrl); // 'https://api.example.com'console.log(config.privateKey); // undefined (private)// Arrow function IIFE (ES6+)(() => { const temp = 'temporary variable'; console.log(temp);})();// Named IIFE for recursion(function factorial(n) { return n <= 1 ? 1 : n * factorial(n - 1);})(5); // 120// IIFE for variable capture in loops (pre-let solution)for (var i = 0; i < 3; i++) { (function(index) { setTimeout(() => console.log(index), 100); })(i); // Capture current value of i}
Example: Module pattern with IIFE
// Classic Module Patternconst Calculator = (function() { // Private variables and functions let history = []; function log(operation, result) { history.push({ operation, result, timestamp: Date.now() }); } // Public API return { add(a, b) { const result = a + b; log('add', result); return result; }, subtract(a, b) { const result = a - b; log('subtract', result); return result; }, getHistory() { return [...history]; // Return copy, not reference }, clearHistory() { history = []; } };})();Calculator.add(5, 3); // 8Calculator.subtract(10, 4); // 6console.log(Calculator.getHistory()); // Array of operationsconsole.log(Calculator.history); // undefined (private)// Revealing Module Patternconst Counter = (function() { let count = 0; function increment() { return ++count; } function decrement() { return --count; } function getValue() { return count; } function reset() { count = 0; } // Reveal selected functions return { increment, decrement, getValue, reset };})();Counter.increment(); // 1Counter.increment(); // 2console.log(Counter.getValue()); // 2// Module with dependenciesconst UserManager = (function($, _) { // Use jQuery ($) and lodash (_) passed as parameters function getUsers() { return _.map(users, user => user.name); } return { getUsers };})(jQuery, lodash); // Inject dependencies
Modern Alternative: ES6 modules (import/export) have largely replaced IIFE-based
modules. However, IIFEs remain useful for: creating private scope, avoiding global pollution, one-time
initialization code, and browser compatibility when modules aren't available.
7. Function Methods (call, apply, bind)
Method
Syntax
Arguments
Returns
When Called
call()
fn.call(thisArg, arg1, arg2, ...)
Individual arguments
Function result
Immediately
apply()
fn.apply(thisArg, [args])
Array of arguments
Function result
Immediately
bind()
fn.bind(thisArg, arg1, ...)
Individual arguments (partial)
New bound function
Later (returned fn)
Use Case
Best Method
Why
Set 'this' and call now
call()
Known number of arguments, immediate execution
Set 'this' with array args
apply()
Arguments already in array, or use Math.max/min with arrays
Set 'this' for later use
bind()
Event handlers, callbacks, partial application
Partial application
bind()
Pre-fill some arguments, returns reusable function
Borrowing methods
call() or apply()
Use array methods on array-like objects
Example: call() and apply()
const person = { name: 'Alice', greet(greeting, punctuation) { return `${greeting}, I'm ${this.name}${punctuation}`; }};console.log(person.greet('Hello', '!')); // "Hello, I'm Alice!"// call() - set 'this' and pass individual argumentsconst otherPerson = { name: 'Bob' };console.log(person.greet.call(otherPerson, 'Hi', '.'));// "Hi, I'm Bob."// apply() - set 'this' and pass arguments as arrayconsole.log(person.greet.apply(otherPerson, ['Hey', '...']));// "Hey, I'm Bob..."// Practical: borrowing array methods for array-like objectsfunction sumAll() { // 'arguments' is array-like, not real array // Borrow reduce() from Array return Array.prototype.reduce.call(arguments, (sum, n) => sum + n, 0);}console.log(sumAll(1, 2, 3, 4)); // 10// Math.max with apply (pre-spread operator)const numbers = [5, 2, 9, 1, 7];const max = Math.max.apply(null, numbers); // 9// Modern: Math.max(...numbers)// Invoking function with specific contextfunction introduce() { return `My name is ${this.name} and I'm ${this.age}`;}const user = { name: 'Charlie', age: 30 };console.log(introduce.call(user));// "My name is Charlie and I'm 30"
Example: bind() for permanent binding
const obj = { value: 42, getValue() { return this.value; }};// Problem: 'this' lost when method assigned to variableconst getValue = obj.getValue;console.log(getValue()); // undefined ('this' is window/undefined)// Solution: bind() creates new function with fixed 'this'const boundGetValue = obj.getValue.bind(obj);console.log(boundGetValue()); // 42// Practical: event handlersclass Button { constructor(label) { this.label = label; this.clickCount = 0; } handleClick() { this.clickCount++; console.log(`${this.label} clicked ${this.clickCount} times`); }}const btn = new Button('Submit');// Without bind, 'this' would be the button element, not our objectdocument.querySelector('button').addEventListener('click', btn.handleClick.bind(btn));// Partial application with bind()function multiply(a, b) { return a * b;}const double = multiply.bind(null, 2); // Pre-fill first argumentconst triple = multiply.bind(null, 3);console.log(double(5)); // 10console.log(triple(5)); // 15// Binding with setTimeoutconst timer = { seconds: 0, start() { // bind() ensures 'this' refers to timer object setInterval(function() { this.seconds++; console.log(this.seconds); }.bind(this), 1000); // Modern alternative: arrow function (lexical 'this') // setInterval(() => { this.seconds++; }, 1000); }};// Multiple bind() calls - first one winsfunction log() { console.log(this.value); }const obj1 = { value: 1 };const obj2 = { value: 2 };const bound1 = log.bind(obj1);const bound2 = bound1.bind(obj2); // Has no effectbound2(); // 1 (not 2 - can't rebind)
Modern Context: Arrow functions (=>) with lexical this have
reduced the need for bind() in many cases. However, call(), apply(), and
bind() remain essential for: method borrowing, explicit context control, partial application, and
working with legacy code.
8. Recursion and Tail Call Optimization
Concept
Definition
Characteristics
Recursion
Function calling itself to solve smaller subproblems
Base case + recursive case; elegant for tree/graph problems
Base Case
Condition that stops recursion without further calls
Prevents infinite recursion; returns direct result
Recursive Case
Logic that breaks problem down and calls function recursively
Progress toward base case; must modify parameters
Call Stack
Stack storing execution contexts for function calls
Each recursive call adds frame; limited size (stack overflow risk)
Tail Call
Function call that's the last operation (nothing after return)
Eligible for optimization; no pending operations
Tail Call Optimization (TCO) LIMITED
Reuse stack frame for tail calls instead of creating new ones
ES6 spec, but poor browser support; prevents stack overflow
Pattern
Stack Usage
TCO Eligible
When to Use
Standard Recursion
O(n) stack frames
✗ No (pending operations after call)
Simple problems, small input size
Tail Recursion
O(1) with TCO, O(n) without
✓ Yes (return is direct recursive call)
Large inputs if TCO available
Accumulator Pattern
O(1) with TCO
✓ Yes (pass result through parameters)
Convert standard recursion to tail recursion
Iteration (Loop)
O(1) stack
N/A (no recursion)
Production code, guaranteed stack safety
Example: Basic recursion patterns
// Standard recursion - NOT tail recursivefunction factorial(n) { // Base case if (n <= 1) return 1; // Recursive case - operation AFTER recursive call return n * factorial(n - 1); // ✗ Not tail call (multiplication pending)}console.log(factorial(5)); // 120// Stack: factorial(5) waits for factorial(4)// factorial(4) waits for factorial(3), etc.// Tail recursive version - uses accumulatorfunction factorialTail(n, acc = 1) { // Base case if (n <= 1) return acc; // Recursive case - nothing after recursive call return factorialTail(n - 1, n * acc); // ✓ Tail call (direct return)}console.log(factorialTail(5)); // 120// With TCO: reuses same stack frame// Fibonacci - standard recursion (exponential time!)function fib(n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); // Two recursive calls}// Very slow for large n: fib(40) takes seconds// Fibonacci - tail recursive with two accumulatorsfunction fibTail(n, a = 0, b = 1) { if (n === 0) return a; return fibTail(n - 1, b, a + b); // Tail call}console.log(fibTail(40)); // Fast even for large n// Array sum - standard recursionfunction sumArray(arr) { if (arr.length === 0) return 0; return arr[0] + sumArray(arr.slice(1)); // ✗ Not tail call}// Array sum - tail recursivefunction sumArrayTail(arr, acc = 0) { if (arr.length === 0) return acc; return sumArrayTail(arr.slice(1), acc + arr[0]); // ✓ Tail call}
Example: Converting to tail recursion
// Problem: countdown - NOT tail recursivefunction countdown(n) { if (n < 0) return; console.log(n); countdown(n - 1); console.log('Done', n); // ✗ Operation after recursive call}// Solution: tail recursive countdownfunction countdownTail(n) { if (n < 0) return; console.log(n); return countdownTail(n - 1); // ✓ Tail call}// Problem: power - NOT tail recursivefunction power(base, exp) { if (exp === 0) return 1; return base * power(base, exp - 1); // ✗ Multiplication pending}// Solution: tail recursive with accumulatorfunction powerTail(base, exp, acc = 1) { if (exp === 0) return acc; return powerTail(base, exp - 1, acc * base); // ✓ Tail call}// Tree traversal - naturally recursivefunction sumTree(node) { if (!node) return 0; return node.value + sumTree(node.left) + sumTree(node.right); // Not tail recursive but appropriate for trees}// Tail recursive tree traversal (using continuation)function sumTreeTail(node, cont = x => x) { if (!node) return cont(0); return sumTreeTail(node.left, leftSum => sumTreeTail(node.right, rightSum => cont(node.value + leftSum + rightSum) ) );}
Important Limitations:
TCO Support: Proper tail call optimization is in ES6 spec but only Safari
implements it. Chrome/Firefox/Edge don't support TCO.
Stack Overflow Risk: Without TCO, even tail-recursive functions will
overflow stack with large inputs (typically 10,000-15,000 calls).
Production Code: For critical code, use iteration (loops) instead of
recursion to guarantee stack safety.
Trampoline: Workaround pattern to simulate TCO, but adds complexity.
Example: When to use recursion vs iteration
// ✓ Good use of recursion - tree/graph structuresfunction findNode(tree, value) { if (!tree) return null; if (tree.value === value) return tree; return findNode(tree.left, value) || findNode(tree.right, value);}// ✓ Good use of recursion - divide and conquerfunction binarySearch(arr, target, left = 0, right = arr.length - 1) { if (left > right) return -1; const mid = Math.floor((left + right) / 2); if (arr[mid] === target) return mid; if (arr[mid] > target) return binarySearch(arr, target, left, mid - 1); return binarySearch(arr, target, mid + 1, right);}// ✗ Bad use of recursion - simple iteration betterfunction sumToN(n) { if (n === 0) return 0; return n + sumToN(n - 1); // Just use n * (n + 1) / 2}// ✓ Better: iterative solutionfunction sumToNIterative(n) { let sum = 0; for (let i = 1; i <= n; i++) { sum += i; } return sum; // Or: return n * (n + 1) / 2;}// Trampoline pattern (TCO workaround)function trampoline(fn) { while (typeof fn === 'function') { fn = fn(); // Keep calling until not a function } return fn;}function factorialTrampoline(n, acc = 1) { if (n <= 1) return acc; return () => factorialTrampoline(n - 1, n * acc); // Return thunk}console.log(trampoline(factorialTrampoline(100000))); // No stack overflow