JavaScript Functions and Functional Programming
1. Function Declaration vs Function Expression Syntax
| Function Type | Syntax | Hoisting | Use Case |
|---|---|---|---|
| Function Declaration | function name(params) { body } |
✓ Fully hoisted (can call before declaration) | Top-level functions, utility functions requiring hoisting |
| Function Expression | const name = function(params) { body }; |
✗ 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 definition
console.log(add(2, 3)); // Works: 5
function add(a, b) {
return a + b;
}
// Function Expression - not hoisted
// console.log(subtract(5, 2)); // Error: Cannot access before initialization
const subtract = function(a, b) {
return a - b;
};
// Named Function Expression - name visible only inside
const 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 syntax
const multiply = (a, b) => a * b;
const square = x => x * x; // Single param, no parens needed
const 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 changes
function 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 functions
class 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 return
const double = x => x * 2;
const sum = (a, b) => a + b;
// Block body - explicit return needed
const greet = name => {
const message = `Hello, ${name}!`;
return message.toUpperCase();
};
// Returning object literal - wrap in parentheses
const makePerson = (name, age) => ({ name, age });
const point = (x, y) => ({ x: x, y: y });
// Array methods with arrows
const 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 arrows
const 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 |
| Using Previous Params | f(a, b = a * 2) |
Default can reference earlier parameters |
| Rest with Destructuring | f([first, ...rest]) |
Combine array destructuring with rest collection |
| Rename in Destructuring | f({x: width, y: height}) |
Extract and rename properties simultaneously |
Example: Default parameters
// Basic default parameters
function greet(name = 'Guest', greeting = 'Hello') {
return `${greeting}, ${name}!`;
}
console.log(greet()); // "Hello, Guest!"
console.log(greet('Alice')); // "Hello, Alice!"
console.log(greet('Bob', 'Hi')); // "Hi, Bob!"
// Default only applies to undefined, not null
console.log(greet(null)); // "Hello, null!" (null !== undefined)
console.log(greet(undefined, 'Hey')); // "Hey, Guest!" (undefined triggers default)
// Default expressions evaluated at call time
let counter = 0;
function f(x = counter++) {
return x;
}
console.log(f()); // 0 (counter becomes 1)
console.log(f()); // 1 (counter becomes 2)
console.log(f(10)); // 10 (default not used, counter stays 2)
// Using previous parameters in defaults
function createRect(width, height = width) {
return { width, height }; // height defaults to width (square)
}
console.log(createRect(5)); // { width: 5, height: 5 }
console.log(createRect(5, 10)); // { width: 5, height: 10 }
Example: Rest parameters
// Rest parameters collect remaining arguments into array
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
console.log(sum()); // 0 (empty array)
// Combine regular params with rest
function 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 object
function 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 rest
const arrowSum = (...nums) => nums.reduce((s, n) => s + n, 0);
Example: Destructuring parameters
// Object destructuring in parameters
function createUser({ name, age, email = 'none@example.com' }) {
return { name, age, email };
}
createUser({ name: 'Alice', age: 30 });
// { name: 'Alice', age: 30, email: 'none@example.com' }
// Array destructuring in parameters
function sumFirstTwo([a, b]) {
return a + b;
}
console.log(sumFirstTwo([10, 20, 30])); // 30
// Nested destructuring
function displayPoint({ coords: { x, y }, label = 'Point' }) {
return `${label}: (${x}, ${y})`;
}
displayPoint({ coords: { x: 10, y: 20 } }); // "Point: (10, 20)"
// Destructuring with rest
function getFirstAndRest([first, ...rest]) {
return { first, rest };
}
console.log(getFirstAndRest([1, 2, 3, 4]));
// { first: 1, rest: [2, 3, 4] }
// Rename while destructuring
function printDimensions({ width: w, height: h }) {
console.log(`Width: ${w}, Height: ${h}`);
}
printDimensions({ width: 100, height: 200 });
// Default for entire destructured object
function config({ host = 'localhost', port = 8080 } = {}) {
return `${host}:${port}`;
}
console.log(config()); // "localhost:8080"
console.log(config({ host: 'example.com' })); // "example.com:8080"
4. Higher-Order Functions and Callbacks
| Concept | Definition | Common Use Cases |
|---|---|---|
| Higher-Order Function (HOF) | Function that takes function(s) as argument(s) and/or returns a function | map, filter, reduce, addEventListener, middleware |
| Callback Function | Function passed as argument to be executed later (synchronous or asynchronous) | Event handlers, async operations, array methods |
| Function Returning Function | HOF that creates and returns new function (function factory) | Configuration, currying, closures, partial application |
| Function Composition | Combining functions where output of one is input to another | Data transformation pipelines, functional programming |
| Built-in HOF | Purpose | Callback Signature | Returns |
|---|---|---|---|
| Array.map() | Transform each element | (element, index, array) => newValue |
New array (same length) |
| Array.filter() | Select elements by condition | (element, index, array) => boolean |
New array (≤ original length) |
| Array.reduce() | Aggregate to single value | (accumulator, element, index, array) => newAcc |
Single accumulated value |
| Array.forEach() | Execute side effects | (element, index, array) => void |
undefined (no return value) |
| Array.find() | Find first matching element | (element, index, array) => boolean |
Element or undefined |
| Array.some() | Test if any element passes | (element, index, array) => boolean |
boolean |
| Array.every() | Test if all elements pass | (element, index, array) => boolean |
boolean |
Example: Higher-order functions
// Function that takes function as argument
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log); // Logs: 0, 1, 2
// Function that returns function (function factory)
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
// Function composition
function compose(f, g) {
return function(x) {
return f(g(x));
};
}
const addOne = x => x + 1;
const square = x => x * x;
const addOneThenSquare = compose(square, addOne);
console.log(addOneThenSquare(4)); // 25 (5²)
// Practical HOF: logger wrapper
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3); // Logs call info, returns 5
Example: Array HOF methods in action
const users = [
{ id: 1, name: 'Alice', age: 25, active: true },
{ id: 2, name: 'Bob', age: 30, active: false },
{ id: 3, name: 'Charlie', age: 35, active: true }
];
// map - transform data
const names = users.map(user => user.name);
// ['Alice', 'Bob', 'Charlie']
// filter - select subset
const activeUsers = users.filter(user => user.active);
// [Alice, Charlie]
// reduce - aggregate
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
// 90
// Chaining HOFs
const result = users
.filter(user => user.active)
.map(user => user.name)
.map(name => name.toUpperCase());
// ['ALICE', 'CHARLIE']
// find - first match
const user = users.find(u => u.age > 25);
// { id: 2, name: 'Bob', age: 30, active: false }
// some - any match
const hasInactive = users.some(u => !u.active); // true
// every - all match
const allActive = users.every(u => u.active); // false
// Complex reduce example
const grouped = users.reduce((groups, user) => {
const key = user.active ? 'active' : 'inactive';
groups[key] = groups[key] || [];
groups[key].push(user);
return groups;
}, {});
// { active: [Alice, Charlie], inactive: [Bob] }
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 variable
function 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()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// 'count' persists between calls through closure
// Multiple closures over same variable
function makeCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count
};
}
const c1 = makeCounter();
const c2 = makeCounter(); // Separate closure, separate count
c1.increment();
c1.increment();
console.log(c1.value()); // 2
console.log(c2.value()); // 0 (independent)
Example: Practical closure patterns
// Function factory with closure
function multiplier(factor) {
return function(number) {
return number * factor; // 'factor' remembered via closure
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Private variables pattern
function 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); // 150
account.withdraw(30); // 120
console.log(account.balance); // undefined (private)
console.log(account.getBalance()); // 120
// Memoization with closure
function 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... 10
fast(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 var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3 (all closures see same 'i')
// ✓ Solution 1: Use let
for (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 value
const 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 Pattern
const 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); // 8
Calculator.subtract(10, 4); // 6
console.log(Calculator.getHistory()); // Array of operations
console.log(Calculator.history); // undefined (private)
// Revealing Module Pattern
const 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(); // 1
Counter.increment(); // 2
console.log(Counter.getValue()); // 2
// Module with dependencies
const 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 arguments
const otherPerson = { name: 'Bob' };
console.log(person.greet.call(otherPerson, 'Hi', '.'));
// "Hi, I'm Bob."
// apply() - set 'this' and pass arguments as array
console.log(person.greet.apply(otherPerson, ['Hey', '...']));
// "Hey, I'm Bob..."
// Practical: borrowing array methods for array-like objects
function 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 context
function 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 variable
const 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 handlers
class 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 object
document.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 argument
const triple = multiply.bind(null, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Binding with setTimeout
const 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 wins
function 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 effect
bound2(); // 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 recursive
function 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 accumulator
function 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 accumulators
function 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 recursion
function sumArray(arr) {
if (arr.length === 0) return 0;
return arr[0] + sumArray(arr.slice(1)); // ✗ Not tail call
}
// Array sum - tail recursive
function 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 recursive
function countdown(n) {
if (n < 0) return;
console.log(n);
countdown(n - 1);
console.log('Done', n); // ✗ Operation after recursive call
}
// Solution: tail recursive countdown
function countdownTail(n) {
if (n < 0) return;
console.log(n);
return countdownTail(n - 1); // ✓ Tail call
}
// Problem: power - NOT tail recursive
function power(base, exp) {
if (exp === 0) return 1;
return base * power(base, exp - 1); // ✗ Multiplication pending
}
// Solution: tail recursive with accumulator
function powerTail(base, exp, acc = 1) {
if (exp === 0) return acc;
return powerTail(base, exp - 1, acc * base); // ✓ Tail call
}
// Tree traversal - naturally recursive
function 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 structures
function 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 conquer
function 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 better
function sumToN(n) {
if (n === 0) return 0;
return n + sumToN(n - 1); // Just use n * (n + 1) / 2
}
// ✓ Better: iterative solution
function 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
Section 6 Summary
- Function types: Declarations (hoisted), expressions (not hoisted), arrows (lexical this), generators (yield), async (await)
- Arrow functions: Concise syntax, lexical
this, noarguments/new/prototype, ideal for callbacks - Parameters: Default values, rest (
...args), destructuring (array/object), combine for flexible APIs - Higher-order functions: Take/return functions; enables map/filter/reduce, composition, abstraction
- Closures: Functions retaining outer scope access; enables private state, factories, memoization
- IIFE: Immediately-invoked functions; creates private scope, module pattern (pre-ES6 modules)
- call/apply/bind: Control
thiscontext; call/apply execute now, bind returns new function - Recursion: Self-calling functions; elegant for trees/graphs; tail recursion + TCO prevents stack overflow (limited support)