Function Declarations & Expressions
JavaScript functions can be defined in multiple ways. Each has different characteristics around hoisting and naming.
// Function Declaration - hoisted, can be called before definition
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5
// Function Expression - not hoisted
const multiply = function(a, b) {
return a * b;
};
// Arrow Function - concise syntax, no own 'this'
const divide = (a, b) => a / b;
// Arrow function with body
const subtract = (a, b) => {
const result = a - b;
return result;
};
// Default parameters
function greet(name, greeting = "Hello") {
return `${greeting}, ${name}!`;
}
console.log(greet("Alice")); // Hello, Alice!
console.log(greet("Bob", "Namaste")); // Namaste, Bob!
// Rest parameters
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
Closures
A closure is a function that "remembers" variables from its outer scope even after the outer function has finished executing. This is one of JavaScript's most powerful features.
// Basic closure example
function makeCounter(initial = 0) {
let count = initial; // Private variable
return {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; },
reset() { count = initial; },
};
}
const counter = makeCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.getCount()); // 11
// Counter instance is independent
const counter2 = makeCounter(0);
console.log(counter2.increment()); // 1
console.log(counter.getCount()); // 11 (unchanged)
// Practical: creating specialized functions
function createMultiplier(factor) {
return (number) => number * factor; // Closes over 'factor'
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const byTen = createMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(byTen(7)); // 70
Higher-Order Functions
Higher-order functions either accept functions as arguments or return functions. They enable powerful functional programming patterns.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map - transform each element
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// filter - keep elements that pass test
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4, 6, 8, 10]
// reduce - accumulate to single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 55
// find - first element that passes test
const firstOver5 = numbers.find(n => n > 5);
// 6
// some / every
const hasNegative = numbers.some(n => n < 0); // false
const allPositive = numbers.every(n => n > 0); // true
// Chaining
const result = numbers
.filter(n => n % 2 === 0) // [2, 4, 6, 8, 10]
.map(n => n * n) // [4, 16, 36, 64, 100]
.reduce((acc, n) => acc + n, 0); // 220
console.log(result); // 220
// Custom higher-order function
function pipe(...fns) {
return (value) => fns.reduce((acc, fn) => fn(acc), value);
}
const process = pipe(
x => x * 2,
x => x + 10,
x => `Result: ${x}`
);
console.log(process(5)); // "Result: 20"
Understanding "this"
The value of this in JavaScript depends on how a function is called, not where it's defined. Arrow functions are an exception — they inherit this from their enclosing scope.
// Regular function - 'this' depends on call site
const obj = {
name: "MyObject",
greet: function() {
console.log(`Hello from ${this.name}`);
},
greetArrow: () => {
// Arrow functions don't have their own 'this'
console.log(`this.name is: ${this?.name}`); // undefined
}
};
obj.greet(); // "Hello from MyObject"
obj.greetArrow(); // "this.name is: undefined"
// Losing 'this' context
const greetFn = obj.greet;
// greetFn(); // 'this' is undefined in strict mode!
// bind, call, apply
const bound = obj.greet.bind({ name: "Bound Context" });
bound(); // "Hello from Bound Context"
obj.greet.call({ name: "Call Context" }); // "Hello from Call Context"
obj.greet.apply({ name: "Apply Context" }); // "Hello from Apply Context"
// Class context (uses 'this' correctly)
class Timer {
constructor() {
this.seconds = 0;
}
start() {
// Arrow function preserves 'this' from class instance
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
}
Use arrow functions for callbacks to avoid "this" binding issues. Regular functions are better for methods that need their own "this".