JavaScript

Functions

Deep dive into JavaScript functions — declarations, expressions, arrow functions, closures, and higher-order functions.

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".