JavaScript Basics II

Functions & Control Flow

INFO 153A/253A - Week 6

UC Berkeley School of Information

Instructor: Kay Ashaolu

Today's Learning Objectives

  • Understand function creation and scope concepts deeply
  • Master conditional logic patterns and decision-making
  • Comprehend different loop types and their applications
  • Learn modern array methods and functional programming
  • See how these concepts form the foundation of web interactivity
Why These Matter:
  • Functions organize code into reusable blocks
  • Control flow enables intelligent program decisions
  • Loops process data collections efficiently
  • Combined, they transform static pages into dynamic experiences

Part 1: Understanding Functions

The Building Blocks of Programming

Functions as Building Blocks:
  • Container for code that executes repeatedly
  • Make programs organized, maintainable, and efficient
  • Like recipes: define steps with different ingredients (parameters)
  • Enable code reuse and logical organization

Function Declaration Fundamentals

// Basic function declaration
function greetUser(name) {
    return "Hello, " + name + "!";
}

// Function invocation
const greeting = greetUser("Alice");
console.log(greeting); // "Hello, Alice!"
Function Anatomy:
  • function greetUser(name) - creates named function with parameter
  • Parameter name acts as placeholder for passed values
  • return statement specifies function output
  • greetUser("Alice") - invocation passes "Alice" to parameter
Key Insight: Parameters are like variables that exist only within the function, and they receive their values from the arguments passed during function invocation.

Function Scope and Variable Access

let globalMessage = "I'm available everywhere";

function demonstrateScope() {
    let localMessage = "I only exist in this function";

    if (true) {
        let blockMessage = "I only exist in this block";
        const alsoBlockScoped = "Me too!";

        console.log(globalMessage);  // ✅ Accessible
        console.log(localMessage);   // ✅ Accessible
        console.log(blockMessage);   // ✅ Accessible
    }

    console.log(globalMessage);      // ✅ Accessible
    console.log(localMessage);       // ✅ Accessible
    // console.log(blockMessage);    // ❌ Error: not defined
}

// console.log(localMessage);       // ❌ Error: not defined
Scope Rules:
  • Scope determines where variables can be accessed
  • Creates boundaries protecting variables from accidental modification
  • Global variables: accessible everywhere
  • Function scope: variables exist only within function
  • Block scope: let/const exist only within curly braces
  • Inner scopes access outer variables (not vice versa)
  • JavaScript uses "scope chain lookup" to find variables

Arrow Functions: Modern JavaScript Syntax

Traditional Function Arrow Function Best Use Case
function add(a, b) { return a + b; } const add = (a, b) => a + b; Simple operations
function square(x) { return x * x; } const square = x => x * x; Single parameter
function process() { /* multiple lines */ } const process = () => { /* multiple lines */ }; No parameters
Arrow Function Benefits:
  • More concise syntax for modern JavaScript
  • Reduces verbosity, especially for callbacks
  • Visual connection between input and body with =>
  • Single parameter: parentheses optional
  • Single expression: automatic return, no braces needed
  • Excellent for data transformation and filtering
  • Common in modern frameworks and functional programming

Default Parameters and Function Flexibility

// Functions with default parameters
function createTask(title, priority = 'medium', dueDate = null) {
    return {
        id: Date.now(),
        title: title,
        priority: priority,
        dueDate: dueDate,
        completed: false
    };
}

// Usage examples showing parameter flexibility
const urgentTask = createTask("Fix critical bug", "high", "2024-10-15");
const normalTask = createTask("Review documentation", "medium");
const simpleTask = createTask("Update README");

console.log(simpleTask);
// { id: 1729123456789, title: "Update README", priority: "medium",
//   dueDate: null, completed: false }
Default Parameters & Factory Functions:
  • Default parameters provide fallback values when arguments missing
  • Make functions flexible and forgiving of different calling patterns
  • Only used when argument is undefined or not provided
  • Factory function pattern creates objects with consistent structure
  • Encapsulates object creation logic and ensures reliability
  • Can provide computed values like unique IDs

Return Values and Function Communication

// Functions that return different types
function validateEmail(email) {
    if (!email || !email.includes('@')) {
        return { valid: false, error: 'Invalid email format' };
    }
    return { valid: true, error: null };
}

function processUserInput(input) {
    // Early returns for different conditions
    if (!input) return null;
    if (input.length < 3) return 'Too short';
    if (input.length > 50) return 'Too long';

    return input.trim().toLowerCase();
}
Return Values & Early Returns:
  • Functions communicate results through return values
  • Objects with multiple properties useful for status + data
  • Early return pattern handles edge cases first
  • Professional technique for readable, maintainable code

Using Function Return Values

// Using return values in practice
const emailCheck = validateEmail('user@example.com');
if (emailCheck.valid) {
    console.log('Email is valid!');
} else {
    console.log('Error:', emailCheck.error);
}

// Chaining function calls
const userInput = processUserInput('  Hello World  ');
if (userInput) {
    console.log('Processed:', userInput); // "hello world"
}

function calculateTotal(items) {
    return items.reduce((sum, item) => sum + item.price, 0);
}
Return Value Patterns:
  • Check return values before using them
  • Chain function calls for data transformation
  • Use consistent return types throughout functions

Part 2: Control Flow Mastery

Making Intelligent Decisions in Code

Control Flow Fundamentals:
  • Determines how programs make decisions and choose execution paths
  • Enables adaptive behavior based on conditions
  • Responds to user input, data conditions, environmental factors
  • Transforms static programs into intelligent, responsive systems

Conditional Logic Patterns

// Basic conditional with comprehensive logic
function determineAccessLevel(user) {
    if (!user) {
        return 'none';
    }

    if (user.role === 'admin') {
        return 'full';
    } else if (user.role === 'moderator') {
        return user.verified ? 'elevated' : 'standard';
    } else if (user.role === 'user') {
        return user.premiumMember ? 'premium' : 'basic';
    } else {
        return 'guest';
    }
}

// Guard clause pattern for cleaner code
function processUser(user) {
    // Handle edge cases first
    if (!user) return null;
    if (!user.id) return null;
    if (user.banned) return { error: 'User is banned' };

    // Main processing logic
    return {
        id: user.id,
        displayName: user.name || 'Anonymous',
        permissions: determineAccessLevel(user)
    };
}
Conditional Logic Patterns:
  • Decision trees handle multiple conditions with nested logic
  • Each branch represents different logical execution path
  • Guard clauses handle exceptional cases first
  • Early returns eliminate deeply nested if statements
  • Makes function requirements explicit
  • Keeps main logic focused on "happy path"

Switch Statements for Multiple Options

// Switch statement for multiple discrete options
function getStatusMessage(status) {
    switch (status.toLowerCase()) {
        case 'pending':
            return 'Your request is being processed';
        case 'approved':
            return 'Request approved! You can proceed';
        case 'rejected':
            return 'Request denied. Please contact support';
        case 'cancelled':
            return 'Request was cancelled by user';
        default:
            return 'Unknown status';
    }
}

// Modern alternative: Object-based lookup
const STATUS_MESSAGES = {
    pending: 'Your request is being processed',
    approved: 'Request approved! You can proceed',
    rejected: 'Request denied. Please contact support',
    cancelled: 'Request was cancelled by user',
    default: 'Unknown status'
};

function getStatusMessageModern(status) {
    return STATUS_MESSAGES[status.toLowerCase()] || STATUS_MESSAGES.default;
}
Switch vs Object Lookup:
  • Switch statements handle multiple discrete options elegantly
  • Avoid long if-else-if chains for single variable
  • Uses strict equality (===) for case comparison
  • Object lookup provides functional alternative
  • Maps inputs to outputs using data instead of code
  • Choose switch for complex logic, objects for simple mapping

Ternary Operator: Concise Conditionals

// Basic ternary usage
const userType = user.premium ? 'Premium User' : 'Standard User';

// Ternary with function calls
const greeting = isLoggedIn ? getPersonalizedGreeting(user) : getDefaultGreeting();

// Nested ternary (use sparingly!)
const priority =
    urgency > 8 ? 'critical' :
    urgency > 5 ? 'high' :
    urgency > 2 ? 'medium' : 'low';

// Ternary for conditional assignment
const theme = user.preferences?.darkMode ? 'dark' : 'light';

// Ternary in template literals
const message = `Welcome, ${user.name || 'Guest'}! You have ${
    user.notifications ? user.notifications.length : 0
} new messages.`;
Ternary Operator Guidelines:
  • Concise conditional logic in single expressions
  • Perfect for conditional assignment
  • Syntax: condition ? value-if-true : value-if-false
  • Always returns one of two values
  • Use cautiously with nesting - prioritize readability
  • If doesn't fit 2-3 lines comfortably, use if-else instead
Readability Warning: Ternary operators should enhance code clarity, not hinder it. If your ternary is complex or hard to understand at a glance, use regular if-else statements instead.

Truthy and Falsy Values in JavaScript

Falsy Values (Only 6!)

  • false - boolean false
  • 0 - number zero
  • "" - empty string
  • null - intentional absence
  • undefined - uninitialized
  • NaN - not a number

Everything Else is Truthy!

  • "0" - string with zero
  • [] - empty array
  • {} - empty object
  • "false" - string "false"
  • -1 - negative numbers
  • Infinity - infinite values
Truthy/Falsy Value Rules:
  • Any value can be used in boolean context
  • Only 6 falsy values: false, 0, "", null, undefined, NaN
  • Everything else is truthy (including [], {})
  • Enables default value assignment with ||
  • Allows simple existence checking
  • Use === for precise comparisons when needed
// Practical examples of truthy/falsy usage
function processInput(input) {
    // Check for any falsy value
    if (!input) {
        return 'No input provided';
    }

    // Default value using short-circuit evaluation
    const name = input.name || 'Anonymous';

    // Existence checking
    if (input.items && input.items.length) {
        return `Processing ${input.items.length} items for ${name}`;
    }

    return `Hello, ${name}!`;
}

Logical Operators and Short-Circuit Evaluation

// Logical AND (&&) - both conditions must be true
const canEdit = user.isAuthenticated && user.hasPermission;

// Logical OR (||) - either condition can be true
const displayName = user.firstName || user.username || 'Guest';

// Logical NOT (!) - reverses truthiness
const isGuest = !user.isAuthenticated;

// Short-circuit evaluation for conditional execution
user.isAdmin && performAdminAction();  // Only runs if user.isAdmin is truthy

// Nullish coalescing (??) - only null or undefined trigger default
const timeout = user.settings?.timeout ?? 5000;  // 0 won't trigger default

// Optional chaining (?.) - safely access nested properties
const email = user.profile?.contact?.email;
Logical Operators & Short-Circuit Evaluation:
  • Operators return actual values, not just true/false
  • && returns first falsy value or last value if all truthy
  • || returns first truthy value or last value if all falsy
  • Short-circuit: stops evaluating once result determined
  • Enables elegant default values and conditional execution
  • ?? (nullish coalescing) more precise than ||
  • ?. (optional chaining) safely accesses nested properties
Modern JavaScript: The nullish coalescing operator (??) and optional chaining (?.) provide more precise control over default values and property access, especially when dealing with 0, empty strings, or nested objects.

Part 3: Loops and Iteration

Processing Collections of Data

Why Loops Matter:
  • Perform repetitive operations efficiently
  • Avoid writing duplicate code
  • Process collections of data systematically
  • Repeat actions until conditions met
  • Modern methods more expressive and less error-prone

Traditional Loops: The Foundation

// For loop - when you need precise control
for (let i = 0; i < 5; i++) {
    console.log(`Count: ${i}`);
}

// While loop - when you don't know iteration count
let userInput = '';
while (userInput !== 'quit') {
    userInput = getUserInput(); // Hypothetical function
    processInput(userInput);
}

// For...of loop - cleaner iteration over arrays
const colors = ['red', 'green', 'blue'];
for (const color of colors) {
    console.log(`Processing color: ${color}`);
}

// For...in loop - for object properties (use carefully)
const person = { name: 'Alice', age: 30, city: 'Berkeley' };
for (const key in person) {
    console.log(`${key}: ${person[key]}`);
}
Traditional Loop Types:
  • for loop: maximum control, explicit initialization/condition/increment
  • while loop: continue until condition false, unknown iteration count
  • for...of loop: clean array iteration, no index management
  • for...in loop: object properties only (avoid with arrays)
  • Modern approach eliminates off-by-one errors
  • Choose based on control needs and data type

Array Methods: Functional Programming

const students = [
    { name: 'Alice', grade: 92, major: 'Computer Science' },
    { name: 'Bob', grade: 78, major: 'Information Science' },
    { name: 'Charlie', grade: 85, major: 'Computer Science' },
    { name: 'Diana', grade: 96, major: 'Information Science' }
];

// forEach - perform action on each element (no return value)
students.forEach(student => {
    console.log(`${student.name}: ${student.grade}%`);
});

// map - transform each element into new array
const studentNames = students.map(student => student.name);
// Result: ['Alice', 'Bob', 'Charlie', 'Diana']

// filter - create new array with elements that pass a test
const csStudents = students.filter(student => student.major === 'Computer Science');
// Result: [Alice and Charlie objects]

// find - get first element that matches condition
const topStudent = students.find(student => student.grade > 90);
// Result: Alice object

// some - check if ANY element matches condition
const hasFailingStudent = students.some(student => student.grade < 60);
// Result: false

// every - check if ALL elements match condition
const allPassing = students.every(student => student.grade >= 70);
// Result: true
Array Methods Benefits:
  • Express intent clearly: "map names", "filter students", "check passing"
  • Each method designed for specific operation type
  • Embrace immutability - don't modify original arrays
  • Return new arrays or computed values
  • Reduce bugs from unintended mutations
  • Enable method chaining for powerful data pipelines

The reduce Method: Ultimate Array Processor

const purchases = [
    { item: 'Coffee', price: 4.50, category: 'beverage' },
    { item: 'Sandwich', price: 8.99, category: 'food' },
    { item: 'Tea', price: 3.25, category: 'beverage' },
    { item: 'Salad', price: 12.50, category: 'food' }
];

// Sum total prices
const totalSpent = purchases.reduce((sum, purchase) => {
    return sum + purchase.price;
}, 0);
// Result: 29.24

// Group items by category
const itemsByCategory = purchases.reduce((groups, purchase) => {
    const category = purchase.category;

    if (!groups[category]) {
        groups[category] = [];
    }

    groups[category].push(purchase.item);
    return groups;
}, {});
// Result: { beverage: ['Coffee', 'Tea'], food: ['Sandwich', 'Salad'] }

// Find most expensive item
const mostExpensive = purchases.reduce((max, purchase) => {
    return purchase.price > max.price ? purchase : max;
});
// Result: Salad object
Reduce Method Mastery:
  • Most powerful array method - transforms to any data type
  • Works through accumulator pattern
  • Accumulator carries forward through each iteration
  • Initial value determines starting point and result type
  • Empty object for grouping, zero for sums
  • Think "fold" - fold entire array into single result
Reduce Pattern: Think of reduce as "fold" - you're folding the entire array into a single result by applying a function repeatedly, carrying forward an accumulated value.

Method Chaining: Creating Data Pipelines

const salesData = [
    { product: 'Laptop', price: 999, category: 'Electronics', inStock: true },
    { product: 'Mouse', price: 25, category: 'Electronics', inStock: false },
    { product: 'Keyboard', price: 75, category: 'Electronics', inStock: true },
    { product: 'Monitor', price: 300, category: 'Electronics', inStock: true },
    { product: 'Webcam', price: 50, category: 'Electronics', inStock: false }
];

// Complex data processing pipeline
const availableElectronicsReport = salesData
    // Step 1: Only items in stock
    .filter(item => item.inStock)

    // Step 2: Sort by price (highest first)
    .sort((a, b) => b.price - a.price)

    // Step 3: Take only top 3 most expensive
    .slice(0, 3)

    // Step 4: Transform for display
    .map((item, index) => ({
        rank: index + 1,
        name: item.product,
        price: `$${item.price}`,
        discountPrice: `$${(item.price * 0.9).toFixed(2)}`
    }))

    // Step 5: Add summary information
    .map(item => ({
        ...item,
        savings: `Save $${(item.price.slice(1) * 0.1).toFixed(2)}`
    }));

console.log('Top Available Electronics:', availableElectronicsReport);
Method Chaining Pipeline Pattern:
  • Connect multiple array methods for data processing
  • Each method receives result of previous method
  • Creates readable flow from raw data to final result
  • Each step has single responsibility
  • Easy to understand, debug, and modify
  • Can add/remove/reorder steps independently
Best Practice: Break long chains across multiple lines with clear comments explaining each step. This makes the data transformation process self-documenting.

Advanced Iteration Patterns

// Working with array indices
const items = ['apple', 'banana', 'cherry'];

// forEach with index
items.forEach((item, index) => {
    console.log(`${index + 1}. ${item}`);
});

// map with index for positioning
const numberedItems = items.map((item, index) => ({
    id: index + 1,
    name: item,
    position: index
}));

// Using entries() for both index and value
for (const [index, item] of items.entries()) {
    console.log(`Position ${index}: ${item}`);
}

// Combining multiple arrays
const names = ['Alice', 'Bob', 'Charlie'];
const ages = [25, 30, 35];
const locations = ['Berkeley', 'Oakland', 'San Francisco'];

const people = names.map((name, index) => ({
    name: name,
    age: ages[index],
    location: locations[index]
}));

// Processing nested arrays
const categories = [
    { name: 'Fruits', items: ['apple', 'banana'] },
    { name: 'Vegetables', items: ['carrot', 'broccoli'] }
];

const allItems = categories.flatMap(category =>
    category.items.map(item => ({
        item,
        category: category.name
    }))
);
Advanced Iteration Patterns:
  • Index parameter provides position information
  • Combine multiple arrays using map with index access
  • Arrays maintain order for related data correlation
  • flatMap processes nested arrays (map + flatten)
  • entries() provides both index and value
  • Common for API responses with separate related arrays

Part 4: Putting It All Together

Understanding How Concepts Interconnect

Integration Concepts:
  • Real applications combine functions, control flow, and loops
  • Create sophisticated program logic through combination
  • Respond to user input dynamically
  • Process complex data systematically
  • Manage application state effectively

Data Validation and Processing Pipeline

// Validation functions for data processing
function createDataProcessor() {
    const validators = {
        required: (value, fieldName) =>
            value ? null : `${fieldName} is required`,

        email: (value) =>
            /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Invalid email format',

        minLength: (value, min) =>
            value.length >= min ? null : `Must be at least ${min} characters`
    };

    return { validators };
}
Validation Pattern:
  • Functions encapsulate specific business rules
  • Return null for valid, error message for invalid
  • Consistent return pattern enables easy checking

Data Processing Pipeline

// Process and validate data collections
function processData(records) {
    return records
        .map(record => ({
            ...record,
            errors: validateRecord(record),
            processed: true,
            timestamp: new Date().toISOString()
        }))
        .filter(record => record.errors.length === 0)
        .sort((a, b) => new Date(a.createdDate) - new Date(b.createdDate));
}

function validateRecord(record) {
    const errors = [];

    // Check required fields
    ['name', 'email'].forEach(field => {
        const error = validators.required(record[field], field);
        if (error) errors.push(error);
    });

    return errors;
}
Processing Pipeline Benefits:
  • Array methods process data collections efficiently
  • Separates concerns: validation, processing, error handling
  • Makes system maintainable and extensible

State Management Patterns

// Core state management functions
function createStateManager(initialState = {}) {
    let state = { ...initialState };
    let listeners = [];

    function getState() {
        return { ...state }; // Return copy to prevent mutations
    }

    function setState(updates) {
        const oldState = { ...state };
        state = { ...state, ...updates };

        // Notify listeners of changes
        listeners.forEach(listener => {
            listener(state, oldState);
        });
    }

    return { getState, setState };
}
State Management Core:
  • Functions encapsulate state operations
  • Closures maintain private state
  • Always create new objects, never modify directly
  • Observer pattern enables reactive updates

Advanced State Operations

// Higher-level state operations with array methods
function addStateHelpers(stateManager) {
    function updateItem(id, updates) {
        const currentState = stateManager.getState();
        stateManager.setState({
            items: currentState.items.map(item =>
                item.id === id ? { ...item, ...updates } : item
            )
        });
    }

    function addItem(newItem) {
        const currentState = stateManager.getState();
        stateManager.setState({
            items: [...currentState.items, { ...newItem, id: Date.now() }]
        });
    }

    function removeItem(id) {
        const currentState = stateManager.getState();
        stateManager.setState({
            items: currentState.items.filter(item => item.id !== id)
        });
    }

    return { updateItem, addItem, removeItem };
}
Immutable Updates:
  • Array methods handle immutable data updates
  • Prevents bugs from unexpected mutations
  • Enables scalable, loosely coupled architecture

Error Handling and Recovery Patterns

// Custom error types for specific scenarios
class ValidationError extends Error {
    constructor(message, field) {
        super(message);
        this.name = 'ValidationError';
        this.field = field;
    }
}

class NetworkError extends Error {
    constructor(message, status) {
        super(message);
        this.name = 'NetworkError';
        this.status = status;
    }
}
Custom Error Types:
  • Custom error classes for specific error types
  • Add context information like field names or status codes
  • Enable different handling based on error type

Error Processing with Arrays

// Process data with comprehensive error handling
function processUserData(users) {
    const results = { successful: [], failed: [], errors: [] };

    users.forEach(user => {
        try {
            if (!user.email) {
                throw new ValidationError('Email is required', 'email');
            }

            const processedUser = {
                ...user,
                id: user.id || Date.now().toString(36),
                processedAt: new Date().toISOString(),
                status: 'active'
            };

            results.successful.push(processedUser);

        } catch (error) {
            results.failed.push(user);
            results.errors.push({
                user: user,
                error: error.message,
                type: error.name
            });
        }
    });

    return results;
}
Error Collection Pattern:
  • Try-catch blocks for error capturing
  • Treat errors as data to be collected and categorized
  • Separate successful from failed operations
  • Provide meaningful feedback instead of crashes

Summary & Key Insights

What You Now Understand:

  • ✅ Functions create reusable, organized code blocks with scope protection
  • ✅ Control flow enables programs to make intelligent decisions
  • ✅ Modern array methods provide powerful data processing capabilities
  • ✅ These concepts combine to create sophisticated application logic
  • ✅ Professional patterns emerge from understanding core principles
Foundation Concepts Mastered:
  • Functions provide code organization and reusability
  • Control flow enables adaptive, intelligent behavior
  • Iteration methods handle data processing efficiently
  • Combined: create responsive, data-driven applications
  • Foundation for all modern web development

The Path Forward

You Can Now Understand:

  • How websites make decisions
  • How data gets processed and displayed
  • How user interactions trigger responses
  • How complex applications are structured
  • How modern frameworks work internally

Next Steps in Your Journey:

  • DOM manipulation and event handling
  • Asynchronous programming concepts
  • Modern framework patterns (React, Vue)
  • API integration and data fetching
  • State management in complex applications
Remember: These concepts are universal programming principles. Whether you're building websites, mobile apps, or server applications, you'll use functions, control flow, and iteration patterns throughout your career.

Questions & Discussion

Let's explore any concepts that need clarification!

Real-World Impact:
  • Functions, control flow, loops make websites interactive
  • Website click responses use these concepts
  • Search result filtering applies these patterns
  • Automatic content updates leverage these principles
  • You now understand the logic behind dynamic websites