Learning Objectives

  • Understand function declarations vs function expressions
  • Master arrow functions and their characteristics
  • Learn parameters, arguments, and default values
  • Work with higher-order functions and callbacks
  • Apply best practices for writing functions

What Are Functions?

Functions are reusable blocks of code that perform a specific task. They're the fundamental building blocks of JavaScript programs, allowing you to organize code, avoid repetition, and create abstractions.

// A simple function
function greet() {
    console.log('Hello, World!');
}

greet(); // 'Hello, World!'

Function Declarations

Function declarations define named functions using the function keyword:

function add(a, b) {
    return a + b;
}

console.log(add(5, 3)); // 8

// Function declarations are hoisted
sayHello(); // Works!

function sayHello() {
    console.log('Hello!');
}

Key characteristics:

  • Hoisted to the top of their scope
  • Can be called before they're defined
  • Must have a name

Function Expressions

Function expressions define functions as part of an expression, typically by assigning them to variables:

const multiply = function(a, b) {
    return a * b;
};

console.log(multiply(4, 5)); // 20

// Function expressions are NOT hoisted
// subtract(); // Error: Cannot access before initialization

const subtract = function(a, b) {
    return a - b;
};

Key characteristics:

  • Not hoisted (when using const/let)
  • Can be anonymous or named
  • Treated as values

Arrow Functions

Arrow functions provide a concise syntax and have special behavior with this:

// Traditional function expression
const square1 = function(x) {
    return x * x;
};

// Arrow function - full syntax
const square2 = (x) => {
    return x * x;
};

// Arrow function - concise syntax (implicit return)
const square3 = x => x * x;

console.log(square3(5)); // 25

// Multiple parameters need parentheses
const add = (a, b) => a + b;

// No parameters need empty parentheses
const random = () => Math.random();

// Returning objects requires parentheses
const makePerson = (name, age) => ({ name, age });

console.log(makePerson('Alice', 30)); // { name: 'Alice', age: 30 }

Arrow function rules:

  • Single parameter: parentheses optional
  • Single expression: braces and return optional
  • No parameters or multiple parameters: parentheses required
  • Don't have their own this binding

Parameters and Arguments

Parameters are variables in the function definition. Arguments are the actual values passed when calling the function:

// Parameters: name, age
function introduce(name, age) {
    console.log(`Hi, I'm ${name} and I'm ${age} years old.`);
}

// Arguments: 'Alice', 30
introduce('Alice', 30); // Hi, I'm Alice and I'm 30 years old.

// Too few arguments - undefined
introduce('Bob'); // Hi, I'm Bob and I'm undefined years old.

// Too many arguments - extras ignored
introduce('Charlie', 25, 'extra'); // Hi, I'm Charlie and I'm 25 years old.

Default Parameters

function greet(name = 'Guest', greeting = 'Hello') {
    console.log(`${greeting}, ${name}!`);
}

greet(); // Hello, Guest!
greet('Alice'); // Hello, Alice!
greet('Bob', 'Hi'); // Hi, Bob!

// Default can be any expression
function createArray(length = 5, fill = 0) {
    return new Array(length).fill(fill);
}

console.log(createArray()); // [0, 0, 0, 0, 0]
console.log(createArray(3, 1)); // [1, 1, 1]

Rest Parameters

Rest parameters collect remaining arguments into an array:

function sum(...numbers) {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15

// Rest parameter must be last
function logInfo(first, second, ...rest) {
    console.log('First:', first);
    console.log('Second:', second);
    console.log('Rest:', rest);
}

logInfo('a', 'b', 'c', 'd', 'e');
// First: a
// Second: b
// Rest: ['c', 'd', 'e']

Return Values

Functions can return values using the return statement:

function multiply(a, b) {
    return a * b;
}

const result = multiply(5, 3);
console.log(result); // 15

// Without return, functions return undefined
function noReturn() {
    console.log('This function returns nothing');
}

console.log(noReturn()); // undefined

// Early return
function divide(a, b) {
    if (b === 0) {
        return 'Cannot divide by zero';
    }
    return a / b;
}

console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // Cannot divide by zero

Higher-Order Functions

Functions that take other functions as arguments or return functions:

// Function that takes a function as argument
function repeat(n, action) {
    for (let i = 0; i < n; i++) {
        action(i);
    }
}

repeat(3, console.log);
// 0
// 1
// 2

// Function that returns a function
function multiplyBy(factor) {
    return function(number) {
        return number * factor;
    };
}

const double = multiplyBy(2);
const triple = multiplyBy(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Callback Functions

Functions passed as arguments to be executed later:

// Array methods use callbacks
const numbers = [1, 2, 3, 4, 5];

// map - transform each element
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter - select elements
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4]

// reduce - combine elements
const sum = numbers.reduce((total, n) => total + n, 0);
console.log(sum); // 15

// forEach - perform action on each
numbers.forEach(n => console.log(n * n));
// 1, 4, 9, 16, 25

// Custom callback example
function processArray(arr, callback) {
    const result = [];
    for (let item of arr) {
        result.push(callback(item));
    }
    return result;
}

const squared = processArray([1, 2, 3], x => x * x);
console.log(squared); // [1, 4, 9]

Immediately Invoked Function Expressions (IIFE)

Functions that execute immediately after being defined:

// Basic IIFE
(function() {
    console.log('This runs immediately!');
})();

// IIFE with parameters
(function(name) {
    console.log(`Hello, ${name}!`);
})('Alice');

// IIFE with return value
const result = (function() {
    const x = 10;
    const y = 20;
    return x + y;
})();

console.log(result); // 30

// Arrow function IIFE
(() => {
    console.log('Arrow IIFE!');
})();

// Use case: Creating private scope
const counter = (function() {
    let count = 0; // Private variable
    
    return {
        increment() {
            count++;
            return count;
        },
        decrement() {
            count--;
            return count;
        },
        getCount() {
            return count;
        }
    };
})();

console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
// console.log(counter.count); // undefined - private!

Function Scope

Variables declared inside a function are local to that function:

function outer() {
    const outerVar = 'I am outer';
    
    function inner() {
        const innerVar = 'I am inner';
        console.log(outerVar); // Can access outer scope
        console.log(innerVar); // Can access own scope
    }
    
    inner();
    // console.log(innerVar); // Error: innerVar not defined
}

outer();

// Closures - inner functions remember outer scope
function makeCounter() {
    let count = 0;
    
    return function() {
        count++;
        return count;
    };
}

const counter1 = makeCounter();
const counter2 = makeCounter();

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 - separate closure

Best Practices

1. Use Descriptive Names

// Bad
function f(x, y) {
    return x + y;
}

// Good
function calculateTotal(price, tax) {
    return price + tax;
}

2. Keep Functions Small and Focused

// Bad - does too much
function processUser(user) {
    validateUser(user);
    saveToDatabase(user);
    sendEmail(user);
    logActivity(user);
}

// Good - single responsibility
function validateUser(user) { /* ... */ }
function saveUser(user) { /* ... */ }
function notifyUser(user) { /* ... */ }

3. Prefer Arrow Functions for Callbacks

// Verbose
numbers.map(function(n) {
    return n * 2;
});

// Concise
numbers.map(n => n * 2);

4. Use Default Parameters Instead of Checking

// Bad
function greet(name) {
    name = name || 'Guest';
    console.log(`Hello, ${name}!`);
}

// Good
function greet(name = 'Guest') {
    console.log(`Hello, ${name}!`);
}

5. Return Early to Reduce Nesting

// Bad - nested
function processValue(value) {
    if (value) {
        if (value > 0) {
            if (value < 100) {
                return value * 2;
            }
        }
    }
    return 0;
}

// Good - early returns
function processValue(value) {
    if (!value) return 0;
    if (value <= 0) return 0;
    if (value >= 100) return 0;
    return value * 2;
}

6. Use Rest Parameters Instead of arguments

// Bad - arguments object
function sum() {
    let total = 0;
    for (let i = 0; i < arguments.length; i++) {
        total += arguments[i];
    }
    return total;
}

// Good - rest parameters
function sum(...numbers) {
    return numbers.reduce((total, n) => total + n, 0);
}

Common Patterns

Function Factory

function createGreeter(greeting) {
    return function(name) {
        return `${greeting}, ${name}!`;
    };
}

const sayHello = createGreeter('Hello');
const sayHi = createGreeter('Hi');

console.log(sayHello('Alice')); // Hello, Alice!
console.log(sayHi('Bob')); // Hi, Bob!

Memoization

function memoize(fn) {
    const cache = {};
    
    return function(...args) {
        const key = JSON.stringify(args);
        if (key in cache) {
            return cache[key];
        }
        const result = fn(...args);
        cache[key] = result;
        return result;
    };
}

const expensiveCalculation = memoize((n) => {
    console.log('Calculating...');
    return n * n;
});

console.log(expensiveCalculation(5)); // Calculating... 25
console.log(expensiveCalculation(5)); // 25 (cached)

Partial Application

function partial(fn, ...fixedArgs) {
    return function(...remainingArgs) {
        return fn(...fixedArgs, ...remainingArgs);
    };
}

function greet(greeting, name) {
    return `${greeting}, ${name}!`;
}

const sayHello = partial(greet, 'Hello');
console.log(sayHello('Alice')); // Hello, Alice!
console.log(sayHello('Bob')); // Hello, Bob!

Key Takeaways

  • Function declarations are hoisted and can be called before definition
  • Function expressions are not hoisted and are treated as values
  • Arrow functions provide concise syntax and lexical this
  • Default parameters provide fallback values
  • Rest parameters collect remaining arguments into an array
  • Higher-order functions take or return other functions
  • Callbacks are functions passed to be executed later
  • IIFEs execute immediately and create private scope
  • Keep functions small, focused, and well-named
  • Use arrow functions for callbacks and short functions