Functions and Scope

Week 1, Day 5 - Lecture 3

Introduction to Functions

Functions are like recipes in programming - they're reusable sets of instructions that perform specific tasks. They take inputs (ingredients), process them (cooking steps), and produce outputs (the finished dish). Functions help us write DRY (Don't Repeat Yourself) code and organize our programs into logical, reusable pieces.

graph LR A[Input] --> B[Function] B --> C[Output] subgraph Function D[Parameters] E[Processing] F[Return Value] end style B fill:#f9f,stroke:#333,stroke-width:2px

Why Use Functions?

Function Declaration

// Function declaration
function greet(name) {
    return "Hello, " + name + "!";
}

// Calling the function
console.log(greet("Alice")); // "Hello, Alice!"

// Function with multiple parameters
function add(a, b) {
    return a + b;
}

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

// Function with no parameters
function getCurrentTime() {
    return new Date().toLocaleTimeString();
}

console.log(getCurrentTime()); // "2:30:45 PM"

// Function with no return value
function logMessage(message) {
    console.log(message);
    // Implicitly returns undefined
}

// Function that returns early
function isPositive(number) {
    if (number > 0) {
        return true;
    }
    return false;
    // Or simply: return number > 0;
}

Function Expressions

// Function expression
const greet = function(name) {
    return "Hello, " + name + "!";
};

// Named function expression
const factorial = function fact(n) {
    if (n <= 1) return 1;
    return n * fact(n - 1); // Can reference itself
};

// Immediately Invoked Function Expression (IIFE)
(function() {
    console.log("This runs immediately!");
})();

// IIFE with parameters
(function(name) {
    console.log("Hello, " + name + "!");
})("Alice");

// Storing functions in objects
const math = {
    add: function(a, b) {
        return a + b;
    },
    subtract: function(a, b) {
        return a - b;
    }
};

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

Arrow Functions

// Basic arrow function
const greet = (name) => {
    return "Hello, " + name + "!";
};

// Concise arrow function (implicit return)
const greetShort = name => "Hello, " + name + "!";

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

// No parameters
const sayHello = () => "Hello!";

// Multiple statements
const calculateTotal = (price, tax) => {
    const taxAmount = price * tax;
    const total = price + taxAmount;
    return total;
};

// Arrow functions with array methods
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);
const evens = numbers.filter(num => num % 2 === 0);
const sum = numbers.reduce((acc, num) => acc + num, 0);

// Lexical 'this' binding
const person = {
    name: "Alice",
    hobbies: ["reading", "coding", "hiking"],
    showHobbies: function() {
        this.hobbies.forEach(hobby => {
            console.log(this.name + " likes " + hobby);
        });
    }
};

Function Parameters

Default Parameters

// Default parameters
function greet(name = "Guest") {
    return "Hello, " + name + "!";
}

console.log(greet());         // "Hello, Guest!"
console.log(greet("Alice")); // "Hello, Alice!"

// Default parameters with expressions
function createUser(name, role = "user", id = Date.now()) {
    return { name, role, id };
}

// Default parameters can reference previous parameters
function createPoint(x = 0, y = x) {
    return { x, y };
}

Rest Parameters

// Rest parameters
function sum(...numbers) {
    return numbers.reduce((acc, num) => acc + num, 0);
}

console.log(sum(1, 2, 3, 4)); // 10

// Rest parameters with other parameters
function introduce(greeting, ...names) {
    return greeting + " " + names.join(", ") + "!";
}

console.log(introduce("Hello", "Alice", "Bob", "Charlie"));
// "Hello Alice, Bob, Charlie!"

// Rest parameters in arrow functions
const multiply = (...args) => args.reduce((a, b) => a * b, 1);

Destructuring Parameters

// Object destructuring in parameters
function createUser({ name, age, email }) {
    return {
        name,
        age,
        email,
        created: new Date()
    };
}

const userData = { name: "Alice", age: 30, email: "alice@example.com" };
console.log(createUser(userData));

// Array destructuring in parameters
function getCoordinates([x, y, z = 0]) {
    return { x, y, z };
}

console.log(getCoordinates([10, 20])); // { x: 10, y: 20, z: 0 }

// Nested destructuring
function processUser({ name, address: { city, country } }) {
    return `${name} from ${city}, ${country}`;
}

Scope in JavaScript

graph TD A[Global Scope] --> B[Function Scope] B --> C[Block Scope] subgraph "Scope Chain" C --> B B --> A end style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#9cf,stroke:#333,stroke-width:2px style C fill:#fc9,stroke:#333,stroke-width:2px

Global Scope

// Global scope
var globalVar = "I'm global";
let globalLet = "I'm also global";
const globalConst = "Me too!";

function showGlobals() {
    console.log(globalVar);  // Accessible
    console.log(globalLet);  // Accessible
    console.log(globalConst); // Accessible
}

// Implicit globals (bad practice!)
function createImplicitGlobal() {
    implicitGlobal = "I'm global without declaration!";
}

Function Scope

function functionScope() {
    var functionVar = "I'm function-scoped";
    let functionLet = "Me too!";
    
    if (true) {
        var insideIf = "I'm still in function scope";
        let blockLet = "I'm block-scoped";
    }
    
    console.log(functionVar); // Works
    console.log(insideIf);    // Works (var ignores blocks)
    // console.log(blockLet); // Error: not defined
}

// console.log(functionVar); // Error: not defined

Block Scope

// Block scope with let and const
if (true) {
    let blockScoped = "I only exist in this block";
    const alsoBlockScoped = "Me too!";
    
    console.log(blockScoped); // Works
}

// console.log(blockScoped); // Error: not defined

// Loop scope
for (let i = 0; i < 3; i++) {
    // i is scoped to this block
    setTimeout(() => console.log(i), 100); // 0, 1, 2
}

// Compare with var
for (var j = 0; j < 3; j++) {
    setTimeout(() => console.log(j), 100); // 3, 3, 3
}

Lexical Scope and Closures

Lexical Scope

// Lexical (static) scope
const globalValue = "global";

function outer() {
    const outerValue = "outer";
    
    function inner() {
        const innerValue = "inner";
        console.log(globalValue); // "global"
        console.log(outerValue);  // "outer"
        console.log(innerValue);  // "inner"
    }
    
    inner();
}

outer();

Closures

// Closure basics
function createCounter() {
    let count = 0;
    
    return function() {
        count++;
        return count;
    };
}

const counter1 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2

const counter2 = createCounter();
console.log(counter2()); // 1 (independent counter)

// Practical closure example
function createGreeting(greeting) {
    return function(name) {
        return `${greeting}, ${name}!`;
    };
}

const sayHello = createGreeting("Hello");
const sayHi = createGreeting("Hi");

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

// Closure with private variables
function createBankAccount(initialBalance) {
    let balance = initialBalance;
    
    return {
        deposit: function(amount) {
            if (amount > 0) {
                balance += amount;
                return balance;
            }
        },
        withdraw: function(amount) {
            if (amount > 0 && amount <= balance) {
                balance -= amount;
                return balance;
            }
        },
        getBalance: function() {
            return balance;
        }
    };
}

const account = createBankAccount(100);
console.log(account.deposit(50));  // 150
console.log(account.withdraw(30)); // 120
console.log(account.getBalance()); // 120
// console.log(account.balance);    // undefined (private)

Function Methods: call, apply, bind

// call method
function greet(greeting, punctuation) {
    return `${greeting}, ${this.name}${punctuation}`;
}

const person = { name: "Alice" };
console.log(greet.call(person, "Hello", "!")); // "Hello, Alice!"

// apply method
const args = ["Hi", "!!"];
console.log(greet.apply(person, args)); // "Hi, Alice!!"

// bind method
const greetAlice = greet.bind(person);
console.log(greetAlice("Hey", ".")); // "Hey, Alice."

// Partial application with bind
function multiply(a, b) {
    return a * b;
}

const double = multiply.bind(null, 2);
console.log(double(5)); // 10

const triple = multiply.bind(null, 3);
console.log(triple(5)); // 15

// Common use case: event handlers
const button = {
    name: "Submit Button",
    handleClick: function() {
        console.log(this.name + " was clicked");
    }
};

// Without bind, 'this' would be the DOM element
// element.addEventListener('click', button.handleClick.bind(button));

Higher-Order Functions

// Function that returns a function
function createMultiplier(multiplier) {
    return function(number) {
        return number * multiplier;
    };
}

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

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

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

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

// Array methods as higher-order functions
const numbers = [1, 2, 3, 4, 5];

const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);

// Custom higher-order function
function compose(f, g) {
    return function(x) {
        return f(g(x));
    };
}

const addOne = x => x + 1;
const double = x => x * 2;
const doubleThenAddOne = compose(addOne, double);

console.log(doubleThenAddOne(5)); // 11

Recursion

// Basic recursion
function factorial(n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

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

// Fibonacci sequence
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Fibonacci with memoization
function fibonacciMemo() {
    const cache = {};
    
    return function fib(n) {
        if (n in cache) return cache[n];
        if (n <= 1) return n;
        
        cache[n] = fib(n - 1) + fib(n - 2);
        return cache[n];
    };
}

const efficientFib = fibonacciMemo();

// Tree traversal
function sumTree(node) {
    if (!node) return 0;
    return node.value + sumTree(node.left) + sumTree(node.right);
}

// Deep object comparison
function deepEqual(obj1, obj2) {
    if (obj1 === obj2) return true;
    
    if (typeof obj1 !== 'object' || obj1 === null ||
        typeof obj2 !== 'object' || obj2 === null) {
        return false;
    }
    
    const keys1 = Object.keys(obj1);
    const keys2 = Object.keys(obj2);
    
    if (keys1.length !== keys2.length) return false;
    
    for (let key of keys1) {
        if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
            return false;
        }
    }
    
    return true;
}

Function Patterns and Best Practices

mindmap root((Function Best Practices)) Design Single responsibility Small and focused Pure functions Descriptive names Parameters Limit count(max 3) Use object for options Default values Validate input Returns Consistent return types Early returns for errors Meaningful values Structure Avoid deep nesting Extract complex logic Use helper functions Keep DRY principle

Pure Functions

// Pure function (no side effects)
function add(a, b) {
    return a + b;
}

// Impure function (has side effects)
let total = 0;
function addToTotal(value) {
    total += value; // Modifies external state
    return total;
}

// Making it pure
function addToTotal(currentTotal, value) {
    return currentTotal + value;
}

Function Composition

// Function composition
const compose = (...fns) => x => 
    fns.reduceRight((v, f) => f(v), x);

const pipe = (...fns) => x => 
    fns.reduce((v, f) => f(v), x);

// Example usage
const addTwo = x => x + 2;
const double = x => x * 2;
const square = x => x * x;

const composedFn = compose(square, double, addTwo);
console.log(composedFn(3)); // ((3 + 2) * 2)² = 100

const pipedFn = pipe(addTwo, double, square);
console.log(pipedFn(3)); // ((3 + 2) * 2)² = 100

Common Function Patterns

Debounce

function debounce(func, delay) {
    let timeoutId;
    
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// Usage
const debouncedSearch = debounce(searchQuery => {
    console.log('Searching for:', searchQuery);
}, 300);

Throttle

function throttle(func, limit) {
    let inThrottle;
    
    return function(...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

// Usage
const throttledScroll = throttle(() => {
    console.log('Scroll event');
}, 100);

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;
    };
}

// Usage
const expensiveOperation = memoize(num => {
    console.log('Computing...');
    return num * num;
});

console.log(expensiveOperation(5)); // Computing... 25
console.log(expensiveOperation(5)); // 25 (cached)

Currying

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        return function(...args2) {
            return curried.apply(this, args.concat(args2));
        };
    };
}

// Usage
const regularAdd = (a, b, c) => a + b + c;
const curriedAdd = curry(regularAdd);

console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6

Assignment: Functions and Scope

  1. Create a function library with:
    • Mathematical operations (factorial, fibonacci, isPrime)
    • String utilities (capitalize, reverse, truncate)
    • Array utilities (unique, flatten, chunk)
    • All functions should be pure
  2. Build a closure-based module:
    • Private variables and methods
    • Public API with getters/setters
    • State management functions
    • Event emitter pattern
  3. Implement higher-order functions:
    • Custom map, filter, reduce
    • Function composition utilities
    • Memoization decorator
    • Debounce and throttle
  4. Create a recursive solution for:
    • Deep object cloning
    • Directory tree walker
    • Nested array flattening
    • JSON parser (simplified)

Bonus: Create a functional programming utility library with compose, pipe, curry, and partial application!