Functions & Closures
Master arrow functions, default parameters, rest and spread operators, and understand how closures work in JavaScript.
Arrow Functions
Arrow functions provide a shorter syntax for writing function expressions and lexically bind the 'this' value from their enclosing scope. They are ideal for callbacks, array methods, and any situation where you want to preserve the outer 'this' context. Arrow functions cannot be used as constructors, do not have their own 'arguments' object, and cannot be used as generator functions. When the function body is a single expression, the braces and return keyword can be omitted for an implicit return.
// Traditional function
const add = function (a, b) {
return a + b;
};
// Arrow function
const addArrow = (a, b) => a + b;
// Single parameter - parens optional
const double = (x) => x * 2;
// No parameters
const greet = () => "Hello!";
// Multi-line body needs braces and return
const calculate = (a, b, operation) => {
const ops = { add: a + b, sub: a - b, mul: a * b };
return ops[operation] ?? 0;
};
// Lexical 'this' binding
class Timer {
constructor() {
this.seconds = 0;
}
start() {
// Arrow function captures 'this' from start()
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
}
// With array methods
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((n) => n * 2);
const evens = numbers.filter((n) => n % 2 === 0);
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(evens); // [2, 4]
Default Parameters
Default parameter values allow function parameters to be initialized with default values when no argument is passed or when undefined is passed. They are evaluated at call time, creating a new object for each invocation, which avoids the mutable default argument pitfall found in other languages. Default parameters can reference previous parameters in the list, enabling powerful initialization patterns. They replace the old pattern of using logical OR to provide fallback values, which had issues with falsy values like 0 or empty strings.
// Default parameters
function createUser(name, role = "viewer", active = true) {
return { name, role, active };
}
console.log(createUser("Alice")); // { name: "Alice", role: "viewer", active: true }
console.log(createUser("Bob", "admin")); // { name: "Bob", role: "admin", active: true }
// Defaults can reference previous params
function createElement(tag, id = tag + "-default", className = "") {
return `<${tag} id="${id}" class="${className}">`;
}
console.log(createElement("div")); // <div id="div-default" class="">
// Default with destructuring
function fetchData({ url, method = "GET", headers = {} } = {}) {
console.log(`${method} ${url}`, headers);
}
fetchData({ url: "/api/users" });
fetchData({ url: "/api/data", method: "POST" });
// Using expressions as defaults
function getTimestamp(date = new Date()) {
return date.toISOString();
}
// Required parameter pattern
function required(name) {
throw new Error(`Parameter ${name} is required`);
}
function createPost(title = required("title"), body = "") {
return { title, body };
}
Rest and Spread Operators
The rest operator (...) collects multiple arguments into an array, replacing the old 'arguments' object with a true array that has access to all array methods. It must be the last parameter in a function definition. The spread operator uses the same syntax but works in the opposite direction, expanding an iterable into individual elements. Spread is commonly used to copy arrays, merge objects, and pass array elements as function arguments.
// Rest parameters
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// Rest with leading params
function logMessage(level, ...messages) {
console.log(`[${level}]`, ...messages);
}
logMessage("INFO", "Server started", "on port 3000");
// Spread with arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// Copy an array (shallow)
const original = [1, 2, 3];
const copy = [...original];
// Spread with objects
const defaults = { theme: "dark", lang: "en", debug: false };
const userPrefs = { theme: "light", fontSize: 16 };
const settings = { ...defaults, ...userPrefs };
console.log(settings);
// { theme: "light", lang: "en", debug: false, fontSize: 16 }
// Spread for function calls
const coords = [51.5, -0.12];
function showLocation(lat, lng) {
console.log(`Lat: ${lat}, Lng: ${lng}`);
}
showLocation(...coords);
Closures
A closure is a function that retains access to variables from its enclosing lexical scope even after the outer function has returned. Every function in JavaScript creates a closure over its surrounding scope. Closures are fundamental to many JavaScript patterns including data privacy, function factories, memoization, and module patterns. Understanding closures is essential for working with callbacks, event handlers, and asynchronous code in JavaScript.
// Basic closure
function createGreeter(greeting) {
return function (name) {
return `${greeting}, ${name}!`;
};
}
const hello = createGreeter("Hello");
const hola = createGreeter("Hola");
console.log(hello("Alice")); // "Hello, Alice!"
console.log(hola("Bob")); // "Hola, Bob!"
// Data privacy with closures
function createCounter(initial = 0) {
let count = initial;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
reset: () => {
count = initial;
},
};
}
const counter = createCounter(10);
counter.increment();
counter.increment();
console.log(counter.getCount()); // 12
// count is not accessible directly
// Memoization
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = memoize((n) => {
console.log("Computing...");
return n * n;
});
console.log(expensiveCalc(5)); // Computing... 25
console.log(expensiveCalc(5)); // 25 (cached)