Function Declaration and Expressions

Understanding the building blocks of reusable code in JavaScript

Introduction to Functions

Functions are one of the fundamental building blocks in JavaScript. They allow you to encapsulate a block of code, give it a name, and reuse it throughout your program. Think of functions as the verbs of programming—they're where the action happens.

The Recipe Analogy

Functions are like recipes in a cookbook:

  • They have a name (function name) so you can find them easily
  • They list what they need (parameters) to work properly
  • They provide detailed instructions (function body) to follow
  • They produce a final dish (return value) as a result
  • They can be used repeatedly without rewriting the recipe every time

Just as a good chef organizes their recipes for different purposes (appetizers, main courses, desserts), a good programmer organizes functions to handle specific tasks in their program.

Why We Use Functions

  • Code Reusability: Write once, use multiple times
  • Modularity: Break complex problems into manageable pieces
  • Abstraction: Hide implementation details behind a simple interface
  • Organization: Structure code in a logical, readable way
  • Maintainability: Update code in one place rather than throughout your program
  • Testability: Test individual functions in isolation

Function Declarations

A function declaration (also called a function statement) defines a named function that is loaded into the execution context before the code runs. This means you can call the function before it appears in your code—a behavior known as hoisting.

Syntax

function functionName(parameter1, parameter2, /* ..., */ parameterN) {
    // Function body: statements that define what the function does
    return expression; // Optional return statement
}

Basic Example

// Function declaration
function greet(name) {
    return `Hello, ${name}!`;
}

// Function call
console.log(greet('Alice')); // Output: Hello, Alice!

Hoisting in Action

Function declarations are hoisted, meaning you can call them before they appear in your code:

// This works even though the function is defined later
console.log(calculateArea(5, 10)); // Output: 50

// Function declaration
function calculateArea(width, height) {
    return width * height;
}

Real-World Example: Form Validation

function validateEmail(email) {
    // Simple regex pattern for email validation
    const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return pattern.test(email);
}

function validatePassword(password) {
    // Password must be at least 8 characters with at least one number
    return password.length >= 8 && /\d/.test(password);
}

function validateForm(email, password) {
    const isEmailValid = validateEmail(email);
    const isPasswordValid = validatePassword(password);
    
    if (!isEmailValid) {
        return 'Please enter a valid email address';
    }
    
    if (!isPasswordValid) {
        return 'Password must be at least 8 characters and contain at least one number';
    }
    
    return 'Form is valid!';
}

// Testing the functions
console.log(validateForm('user@example.com', 'password123')); // Form is valid!
console.log(validateForm('not-an-email', 'password123')); // Please enter a valid email address
console.log(validateForm('user@example.com', 'password')); // Password must be at least 8 characters...

When to Use Function Declarations

  • When you need to use a function before it's defined (thanks to hoisting)
  • For main functions that form the core of your application
  • When clarity and readability are more important than conciseness
  • When you want to make your code's structure obvious at a glance

Function Expressions

A function expression defines a function as part of a larger expression, typically a variable assignment. Unlike function declarations, function expressions are not hoisted, so they cannot be used before they are defined.

Syntax

const functionName = function(parameter1, parameter2, /* ..., */ parameterN) {
    // Function body
    return expression; // Optional
};

Basic Example

// Function expression
const add = function(a, b) {
    return a + b;
};

// Function call
console.log(add(5, 3)); // Output: 8

No Hoisting with Function Expressions

// This will cause an error
// console.log(subtract(10, 5)); // Error: subtract is not a function

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

// This works because the function is already defined
console.log(subtract(10, 5)); // Output: 5

Anonymous Function Expressions

Function expressions often don't include a name (making them anonymous), but you can name them if needed:

// Anonymous function expression
const sayHello = function() {
    console.log('Hello, world!');
};

// Named function expression
const factorial = function calcFactorial(n) {
    // The name 'calcFactorial' is only available inside the function
    if (n <= 1) return 1;
    return n * calcFactorial(n - 1); // Recursion using the function name
};

console.log(factorial(5)); // Output: 120

Real-World Example: Event Handlers

// Function expression as an event handler
document.getElementById('myButton').addEventListener('click', function(event) {
    console.log('Button clicked!');
    event.preventDefault();
    
    // Perform some action
    updateUserInterface();
});

// Helper function
function updateUserInterface() {
    // Update UI logic here
    console.log('UI updated after button click');
}

When to Use Function Expressions

  • When you want to assign a function to a variable or pass it as an argument
  • For callback functions that won't be reused elsewhere
  • When you need to create closures (functions that "remember" their creation environment)
  • When you want to ensure the function is only used after definition

Arrow Functions

Arrow functions are a more concise syntax for writing function expressions, introduced in ES6 (ECMAScript 2015). They have some important differences from traditional functions, particularly regarding this binding and lack of their own arguments object.

Syntax

// Basic syntax
const functionName = (param1, param2, ...) => expression;

// With function body
const functionName = (param1, param2, ...) => {
    // Function body
    return expression;
};

Basic Examples

// Arrow function with implicit return
const double = x => x * 2;

// Arrow function with multiple parameters
const multiply = (a, b) => a * b;

// Arrow function with function body and explicit return
const divide = (a, b) => {
    if (b === 0) {
        throw new Error('Cannot divide by zero');
    }
    return a / b;
};

console.log(double(4));        // Output: 8
console.log(multiply(3, 5));   // Output: 15
console.log(divide(10, 2));    // Output: 5

Comparing Function Expressions and Arrow Functions

Traditional Function Expression

const numbers = [1, 2, 3, 4, 5];

// Using a function expression
const squares1 = numbers.map(function(num) {
    return num * num;
});

console.log(squares1); // [1, 4, 9, 16, 25]

Arrow Function

const numbers = [1, 2, 3, 4, 5];

// Using an arrow function
const squares2 = numbers.map(num => num * num);

console.log(squares2); // [1, 4, 9, 16, 25]

The "this" Binding in Arrow Functions

A key difference with arrow functions is how they handle the this keyword. Arrow functions don't have their own this context—they inherit this from the surrounding code:

flowchart TD A["Traditional Function:
'this' is determined
when function is called"] B["Arrow Function:
'this' is inherited from
surrounding scope"] C["const obj = {
name: 'Example',
regularMethod() { /* this = obj */ },
arrowMethod: () => { /* this = parent scope */ }
}"]

Traditional Function: New this Context

const person = {
    name: 'Alice',
    regularFunction: function() {
        console.log(this.name);
    }
};

person.regularFunction(); // Output: Alice

Arrow Function: Inherited this

const person = {
    name: 'Alice',
    arrowFunction: () => {
        console.log(this.name); // 'this' is from parent scope
    }
};

person.arrowFunction(); // Output: undefined (in browser, 
// 'this' refers to window)

Common Use Case: Preserving this in Callbacks

const counter = {
    count: 0,
    
    // Problem: 'this' is lost in setTimeout callback
    startWithRegular: function() {
        console.log("Starting regular function timer...");
        setTimeout(function() {
            this.count++; // 'this' refers to the timeout context, not counter
            console.log(this.count); // NaN (window.count is undefined)
        }, 1000);
    },
    
    // Solution: Arrow function preserves 'this'
    startWithArrow: function() {
        console.log("Starting arrow function timer...");
        setTimeout(() => {
            this.count++; // 'this' refers to counter
            console.log(this.count); // 1
        }, 1000);
    }
};

counter.startWithRegular(); // NaN after 1 second
counter.startWithArrow();   // 1 after 1 second

Limitations of Arrow Functions

  • No arguments object: Arrow functions don't have their own arguments object (use rest parameters instead)
  • Cannot be used as constructors: Arrow functions cannot be used with new
  • No super keyword: Arrow functions don't have access to super
  • Cannot change this binding: Methods like call(), apply(), and bind() won't change this in arrow functions
// No arguments object in arrow functions
const regularFunc = function() {
    console.log(arguments);
};

const arrowFunc = () => {
    console.log(arguments); // Error or refers to parent scope's arguments
};

regularFunc(1, 2, 3); // Arguments object: [1, 2, 3]
// arrowFunc(1, 2, 3); // Error or unexpected behavior

// Use rest parameters instead
const betterArrowFunc = (...args) => {
    console.log(args); // Works as expected
};

betterArrowFunc(1, 2, 3); // Output: [1, 2, 3]

When to Use Arrow Functions

  • For short, simple functions, especially callbacks
  • When you want to preserve the this context from the surrounding code
  • For functional programming techniques with array methods (map, filter, reduce)
  • When you need concise, one-line functions

When Not to Use Arrow Functions

  • For methods in objects or classes that need their own this context
  • For constructor functions
  • When you need access to the arguments object
  • For functions that use yield (generators)

Immediately Invoked Function Expressions (IIFE)

An IIFE (pronounced "iffy") is a function that runs as soon as it's defined. It's a design pattern used to create a new scope and avoid polluting the global namespace.

Syntax

// Basic IIFE syntax
(function() {
    // Code executed immediately
})();

// IIFE with parameters
(function(param1, param2) {
    // Code executed immediately
})(value1, value2);

Basic Example

// IIFE that computes and logs the result immediately
(function() {
    const x = 10;
    const y = 20;
    console.log(x + y); // Output: 30
})();

// Trying to access variables defined in the IIFE
console.log(typeof x); // Output: undefined (x is not in scope)

Common Use Cases for IIFEs

  1. Avoiding Global Variables: Create a private scope to avoid polluting the global namespace
  2. Module Pattern: Create modules with private and public parts
  3. Isolating Declarations: Run code without interference from other scripts
  4. Capturing Variable Values: Preserve variable values in loops or asynchronous code

Real-World Example: Module Pattern with IIFE

// Create a counter module with private variables
const counter = (function() {
    // Private variables
    let count = 0;
    
    // Return an object with public methods
    return {
        increment: function() {
            count += 1;
            return count;
        },
        decrement: function() {
            count -= 1;
            return count;
        },
        getValue: function() {
            return count;
        },
        reset: function() {
            count = 0;
            return count;
        }
    };
})();

// Using the counter module
console.log(counter.getValue()); // Output: 0
console.log(counter.increment()); // Output: 1
console.log(counter.increment()); // Output: 2
console.log(counter.decrement()); // Output: 1
console.log(counter.reset());     // Output: 0

// The 'count' variable is private and not accessible
console.log(counter.count);       // Output: undefined

Modern Alternative: ES6 Modules

With the introduction of ES6 modules, many use cases for IIFEs are now better handled with proper module imports and exports:

counter.js

// Private variable within the module scope
let count = 0;

// Exported functions
export function increment() {
    count += 1;
    return count;
}

export function decrement() {
    count -= 1;
    return count;
}

export function getValue() {
    return count;
}

export function reset() {
    count = 0;
    return count;
}

main.js

// Import the functions from the counter module
import { increment, decrement, getValue, reset } from './counter.js';

console.log(getValue());  // Output: 0
console.log(increment()); // Output: 1
console.log(increment()); // Output: 2
console.log(decrement()); // Output: 1
console.log(reset());     // Output: 0

Comparing Function Types

Let's compare all the function types we've covered to help you choose the right approach for different situations.

Function Declaration function add(a, b) { return a + b; } Function Expression const add = function(a, b) { return a + b; }; Arrow Function const add = (a, b) => a + b; IIFE (Immediately Invoked Function Expression) (function(a, b) { console.log(a + b); })(2, 3);

Feature Comparison

Feature Function Declaration Function Expression Arrow Function IIFE
Hoisting Yes No No N/A (runs immediately)
Own this binding Yes Yes No (inherits from parent) Yes
arguments object Yes Yes No Yes
Can be used as constructor Yes Yes No Technically yes, but not practical
Can be named Yes (required) Optional No Optional
Conciseness Medium Medium High Low

Quick Decision Guide

flowchart TD A[Need a function] --> B{Need to use it before definition?} B -->|Yes| C[Function Declaration] B -->|No| D{Need this binding?} D -->|Yes| E[Function Expression] D -->|No| F{One-line simple function?} F -->|Yes| G[Arrow Function] F -->|No| H{Need private scope?} H -->|Yes| I[IIFE] H -->|No| J{Event handler or callback?} J -->|Yes| K[Arrow Function] J -->|No| L[Function Expression]

Common Function Patterns

Beyond the basic syntax, several function patterns appear frequently in JavaScript development.

Callback Functions

A callback function is passed as an argument to another function and is executed after some operation has been completed.

// Function that takes a callback
function fetchData(callback) {
    console.log('Fetching data...');
    
    // Simulate asynchronous operation
    setTimeout(() => {
        const data = { id: 1, name: 'Example Data' };
        callback(data);
    }, 1000);
}

// Using the function with a callback
fetchData(function(data) {
    console.log('Data received:', data);
});

// Using an arrow function as callback
fetchData(data => {
    console.log('Data received with arrow function:', data);
});

Higher-Order Functions

Higher-order functions either take functions as arguments or return functions as results.

// Higher-order function that returns a function
function multiplier(factor) {
    // Returns a function that multiplies its argument by factor
    return function(number) {
        return number * factor;
    };
}

// Create specific multiplier functions
const double = multiplier(2);
const triple = multiplier(3);

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

// Higher-order function that takes a function as an argument
function applyOperation(numbers, operation) {
    const results = [];
    for (const num of numbers) {
        results.push(operation(num));
    }
    return results;
}

const numbers = [1, 2, 3, 4, 5];
console.log(applyOperation(numbers, double));  // Output: [2, 4, 6, 8, 10]
console.log(applyOperation(numbers, n => n * n));  // Output: [1, 4, 9, 16, 25]

Function Composition

Function composition involves combining multiple functions to create a new function where the output of one function becomes the input to the next.

// Simple functions to compose
function addTen(x) {
    return x + 10;
}

function double(x) {
    return x * 2;
}

function square(x) {
    return x * x;
}

// Manual composition
const result = square(double(addTen(5)));
console.log(result); // Output: 900 (5 + 10 = 15, 15 * 2 = 30, 30² = 900)

// Helper function for composition
function compose(...functions) {
    return function(x) {
        return functions.reduceRight((acc, fn) => fn(acc), x);
    };
}

// Create composed function
const addThenDoubleAndSquare = compose(square, double, addTen);

console.log(addThenDoubleAndSquare(5)); // Output: 900

Currying

Currying is a technique of transforming a function that takes multiple arguments into a sequence of functions that each take a single argument.

// Regular function with multiple arguments
function calculateVolume(length, width, height) {
    return length * width * height;
}

// Curried version
function curriedVolume(length) {
    return function(width) {
        return function(height) {
            return length * width * height;
        };
    };
}

// Using the curried function
console.log(calculateVolume(2, 3, 4)); // Output: 24
console.log(curriedVolume(2)(3)(4));   // Output: 24

// Partial application
const volumeWithLength2 = curriedVolume(2);
const volumeWithLength2Width3 = volumeWithLength2(3);

console.log(volumeWithLength2Width3(4)); // Output: 24

// Arrow function version (more concise)
const curriedVolumeArrow = length => width => height => length * width * height;
console.log(curriedVolumeArrow(2)(3)(4)); // Output: 24

Real-World Example: Form Validation System

Combining different function patterns to create a flexible validation system:

// Higher-order function to create validators
function createValidator(validationFn, errorMessage) {
    return function(value) {
        const isValid = validationFn(value);
        return {
            isValid,
            message: isValid ? null : errorMessage
        };
    };
}

// Simple validation functions
const isNotEmpty = value => !!value && value.trim().length > 0;
const isEmail = value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
const isMinLength = length => value => value.length >= length;

// Create specific validators using the higher-order function
const validateRequired = createValidator(isNotEmpty, 'This field is required');
const validateEmail = createValidator(isEmail, 'Please enter a valid email address');
const validatePassword = createValidator(isMinLength(8), 'Password must be at least 8 characters');

// Function composition to run multiple validators
function runValidators(value, validators) {
    for (const validator of validators) {
        const result = validator(value);
        if (!result.isValid) {
            return result;
        }
    }
    return { isValid: true, message: null };
}

// Test the validation system
function validateForm(formData) {
    const validationResults = {
        name: runValidators(formData.name, [validateRequired]),
        email: runValidators(formData.email, [validateRequired, validateEmail]),
        password: runValidators(formData.password, [validateRequired, validatePassword])
    };
    
    // Check if all validations passed
    const isFormValid = Object.values(validationResults)
        .every(result => result.isValid);
    
    return {
        isValid: isFormValid,
        fields: validationResults
    };
}

// Test with valid data
const validData = {
    name: 'John Doe',
    email: 'john@example.com',
    password: 'password123'
};

// Test with invalid data
const invalidData = {
    name: 'Jane',
    email: 'not-an-email',
    password: 'short'
};

console.log(validateForm(validData));
console.log(validateForm(invalidData));

Best Practices for Functions

Writing clean, maintainable functions is an essential skill for JavaScript developers. Here are some best practices to follow:

Follow the Single Responsibility Principle

Each function should do one thing and do it well.

❌ Poor Practice

function processUserData(userData) {
    // Validate data
    if (!userData.name) throw new Error('Name is required');
    
    // Update database
    database.update(userData);
    
    // Send welcome email
    sendEmail(userData.email, 'Welcome!');
    
    // Update UI
    updateUserInterface(userData);
}

✅ Better Practice

function validateUserData(userData) {
    if (!userData.name) throw new Error('Name is required');
    return userData;
}

function updateUserInDatabase(userData) {
    return database.update(userData);
}

function sendWelcomeEmail(email) {
    return sendEmail(email, 'Welcome!');
}

function updateUserInterface(userData) {
    // UI update logic
}

// Orchestrating function
function processUserData(userData) {
    const validatedData = validateUserData(userData);
    updateUserInDatabase(validatedData);
    sendWelcomeEmail(validatedData.email);
    updateUserInterface(validatedData);
}

Keep Functions Small and Focused

Aim for functions that are short, focused, and readable at a glance.

  • Functions should typically be 10-20 lines of code
  • If a function grows larger, consider refactoring into smaller functions
  • Follow the "do one thing" rule: each function should solve a single problem

Use Descriptive Function Names

Function names should clearly describe what the function does, using verb-noun format when appropriate.

❌ Poor Naming

function process(data) { /* ... */ }
function handle(user, action) { /* ... */ }
function execute() { /* ... */ }

✅ Better Naming

function validateFormData(data) { /* ... */ }
function updateUserPermissions(user, action) { /* ... */ }
function fetchUserProfile() { /* ... */ }

Limit the Number of Parameters

Too many parameters make functions harder to use and understand. Aim for 3 or fewer parameters.

❌ Too Many Parameters

function createUser(
    name, 
    email, 
    password, 
    age, 
    location, 
    preferences, 
    role, 
    status
) { /* ... */ }

✅ Object Parameter Pattern

function createUser(userData) {
    // Destructure with defaults
    const { 
        name, 
        email, 
        password,
        age = 18,
        location = 'Unknown',
        preferences = {},
        role = 'user',
        status = 'active'
    } = userData;
    
    // Function logic
}

Return Early to Avoid Deep Nesting

Use early returns (guard clauses) to handle edge cases and reduce indentation.

❌ Deeply Nested Approach

function processPayment(payment) {
    if (payment) {
        if (payment.amount > 0) {
            if (payment.method) {
                // Process the payment
                return true;
            } else {
                console.error('Payment method missing');
                return false;
            }
        } else {
            console.error('Invalid payment amount');
            return false;
        }
    } else {
        console.error('Payment object missing');
        return false;
    }
}

✅ Early Return Approach

function processPayment(payment) {
    // Guard clauses for validation
    if (!payment) {
        console.error('Payment object missing');
        return false;
    }
    
    if (payment.amount <= 0) {
        console.error('Invalid payment amount');
        return false;
    }
    
    if (!payment.method) {
        console.error('Payment method missing');
        return false;
    }
    
    // Main function logic
    // Process the payment
    return true;
}

Use Default Parameters

Default parameters make functions more robust and reduce the need for additional logic.

// Without default parameters
function createUser(name, email, role) {
    // Check for missing parameters
    if (role === undefined) {
        role = 'user';
    }
    
    return { name, email, role };
}

// With default parameters
function createUser(name, email, role = 'user') {
    return { name, email, role };
}

console.log(createUser('Alice', 'alice@example.com'));
// Output: { name: 'Alice', email: 'alice@example.com', role: 'user' }

Document Your Functions

Use comments to explain the purpose, parameters, and return value of your functions, especially for complex ones.

/**
 * Calculates the discounted price based on the original price and discount percentage.
 *
 * @param {number} price - The original price
 * @param {number} discountPercent - The discount percentage (0-100)
 * @returns {number} The discounted price
 * @throws {Error} If price is negative or discount is invalid
 */
function calculateDiscountedPrice(price, discountPercent = 0) {
    if (price < 0) {
        throw new Error('Price cannot be negative');
    }
    
    if (discountPercent < 0 || discountPercent > 100) {
        throw new Error('Discount must be between 0 and 100');
    }
    
    const discount = price * (discountPercent / 100);
    return price - discount;
}

Practical Exercise: Building a Calculator Library

Let's put our knowledge of functions into practice by building a calculator library that demonstrates different function types and patterns.

// Calculator library using different function types
const Calculator = (function() {
    // Private utilities using function declarations
    function validateNumbers(...args) {
        return args.every(arg => typeof arg === 'number' && !isNaN(arg));
    }
    
    function formatResult(result) {
        // Round to 2 decimal places if necessary
        return Math.round(result * 100) / 100;
    }
    
    // Basic operations using function expressions
    const add = function(a, b) {
        if (!validateNumbers(a, b)) {
            throw new Error('Both arguments must be numbers');
        }
        return a + b;
    };
    
    const subtract = function(a, b) {
        if (!validateNumbers(a, b)) {
            throw new Error('Both arguments must be numbers');
        }
        return a - b;
    };
    
    // Advanced operations using arrow functions
    const multiply = (a, b) => {
        if (!validateNumbers(a, b)) {
            throw new Error('Both arguments must be numbers');
        }
        return a * b;
    };
    
    const divide = (a, b) => {
        if (!validateNumbers(a, b)) {
            throw new Error('Both arguments must be numbers');
        }
        if (b === 0) {
            throw new Error('Cannot divide by zero');
        }
        return a / b;
    };
    
    // Higher-order function for creating power functions
    function createPowerFn(exponent) {
        return function(base) {
            if (!validateNumbers(base, exponent)) {
                throw new Error('Arguments must be numbers');
            }
            return Math.pow(base, exponent);
        };
    }
    
    // Create specialized power functions
    const square = createPowerFn(2);
    const cube = createPowerFn(3);
    
    // Function that takes a callback
    function calculate(a, b, operation) {
        try {
            const result = operation(a, b);
            return formatResult(result);
        } catch (error) {
            console.error('Calculation error:', error.message);
            return null;
        }
    }
    
    // Chain calculations using a builder pattern
    function chain(initialValue = 0) {
        let result = initialValue;
        
        return {
            add: function(value) {
                result = add(result, value);
                return this;
            },
            subtract: function(value) {
                result = subtract(result, value);
                return this;
            },
            multiply: function(value) {
                result = multiply(result, value);
                return this;
            },
            divide: function(value) {
                result = divide(result, value);
                return this;
            },
            square: function() {
                result = square(result);
                return this;
            },
            cube: function() {
                result = cube(result);
                return this;
            },
            value: function() {
                return formatResult(result);
            }
        };
    }
    
    // Public API
    return {
        add,
        subtract,
        multiply,
        divide,
        square,
        cube,
        calculate,
        chain,
        // Add a power method that uses currying
        power: base => exponent => Math.pow(base, exponent)
    };
})();

// Using the calculator library
console.log('Basic operations:');
console.log('5 + 3 =', Calculator.add(5, 3));
console.log('10 - 4 =', Calculator.subtract(10, 4));
console.log('7 * 6 =', Calculator.multiply(7, 6));
console.log('20 / 4 =', Calculator.divide(20, 4));

console.log('\nPower functions:');
console.log('Square of 5 =', Calculator.square(5));
console.log('Cube of 3 =', Calculator.cube(3));
console.log('2 to the power of 8 =', Calculator.power(2)(8));

console.log('\nCalculate with callbacks:');
console.log('8 + 2 =', Calculator.calculate(8, 2, Calculator.add));
console.log('8 - 2 =', Calculator.calculate(8, 2, Calculator.subtract));
console.log('8 * 2 =', Calculator.calculate(8, 2, Calculator.multiply));
console.log('8 / 2 =', Calculator.calculate(8, 2, Calculator.divide));
console.log('8 / 0 =', Calculator.calculate(8, 0, Calculator.divide)); // Error handling

console.log('\nChain calculations:');
const result = Calculator.chain(10)
    .add(5)        // 15
    .multiply(2)   // 30
    .subtract(8)   // 22
    .divide(2)     // 11
    .square()      // 121
    .value();

console.log('Chain result:', result);

// Advanced usage: create custom operation
const average = (a, b) => (a + b) / 2;
console.log('\nCustom operation (average):', Calculator.calculate(10, 20, average));

Challenge Extensions

  1. Add more operations like modulo, percentage, and square root
  2. Extend the calculator to handle arrays of numbers
  3. Add memory functions (store, recall, clear memory)
  4. Implement a history feature that stores previous calculations
  5. Create a scientific calculator with trigonometric functions

Summary

In this lecture, we've explored the different ways to create and use functions in JavaScript:

We also covered common function patterns and best practices that will help you write clean, maintainable code.

Functions are one of the most powerful features in JavaScript, allowing you to:

By mastering the different types of functions and when to use them, you'll be well-equipped to tackle a wide range of programming challenges.

Further Learning

Practice Activities

  1. Function Type Converter: Take existing JavaScript code and rewrite functions using different function types (declaration to expression, expression to arrow, etc.).
  2. Utility Library: Create a library of utility functions for string and array manipulation using a mixture of function types.
  3. Higher-Order Function Challenge: Implement map, filter, and reduce functions from scratch that work like the native array methods.
  4. Currying and Composition: Create a set of functions that can be composed and curried to process text data in different ways.
  5. Event Manager: Build an event system with subscribe and publish functions that demonstrates callbacks and closure concepts.