HTML, CSS & JavaScript

Chapter 10
Functions, Collections and Program Structure

Certificate in Full stack programming
HTML, CSS & JavaScript   •   Certificate

Functions, Collections and Program Structure

Introduction

As JavaScript programs grow beyond a few lines, placing all instructions in one sequence becomes difficult to understand, test, and maintain. Functions allow developers to name and reuse behaviour; collections provide structured ways to store related data; and modules divide a program into manageable files. Together, these features form the foundation of maintainable client-side and server-side applications.

This chapter examines JavaScript functions, parameter passing, return values, scope, closures, arrow functions, strings, arrays, objects, destructuring, common array methods, and modules. The emphasis is on practical programming decisions: choosing an appropriate data structure, avoiding unintended mutation, designing reusable functions, and organizing code so that its responsibilities are clear.

Learning Objectives

By the end of this chapter, you should be able to:

  • Define and invoke function declarations, expressions, and arrow functions.
  • Use parameters, default values, rest parameters, and return values.
  • Explain lexical scope, block scope, closures, and common scope errors.
  • Manipulate strings, arrays, and objects safely and effectively.
  • Apply map(), filter(), reduce(), find(), some(), and every().
  • Use object and array destructuring, spread syntax, and computed properties.
  • Divide application logic into ES modules using export and import.
  • Select appropriate structures and design reusable, readable program logic.

1. Functions as Reusable Behaviour

A function is a named or anonymous unit of instructions that can receive input, perform an operation, and optionally produce an output. Functions reduce duplication and give a program meaningful abstractions.

Function declarations

function calculateTotal(price, quantity) {
  return price * quantity;
}

const total = calculateTotal(12.5, 4); // 50

The function has two parameters, price and quantity. When it is called, the supplied values are arguments. return ends the function and sends a value to the caller. A function without an explicit return produces undefined.

Function declarations are hoisted: JavaScript makes the declaration available before its position in the source file during the creation of the execution context. Nevertheless, placing declarations before their use is usually clearer.

Function expressions

A function can be stored in a variable:

const formatName = function (first, last) {
  return `${first} ${last}`.trim();
};

Function expressions are not usable before the assignment has executed when the variable is declared with const or let. They are useful when a function is treated as data, for example when passed as a callback.

Parameters and arguments

Parameters may have default values:

function greet(name = "Guest") {
  return `Hello, ${name}`;
}

A rest parameter gathers remaining arguments into an array. It must be the final parameter:

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

JavaScript passes primitive values such as numbers, strings, booleans, null, undefined, bigint, and symbols by value. Objects, arrays, and functions are values that contain references to mutable data. Reassigning a parameter does not replace the caller's variable, but mutating an object through that parameter can affect the original object.

function rename(user) {
  user.name = "Updated";       // mutates the object
}

function replaceUser(user) {
  user = { name: "New user" }; // changes only the local parameter
}

This distinction is important when designing APIs. A function should document whether it mutates its inputs. In many applications, returning a new object or array is safer and easier to reason about.

Return values and early returns

A function should normally perform one clearly defined task. Early returns can reduce nesting and make validation explicit:

function getDiscount(price, member) {
  if (price <= 0) return 0;
  if (!member) return 0;
  return price * 0.1;
}

Returning a value does not display it. A caller must store, print, render, or otherwise use the result.

2. Arrow Functions

Arrow functions provide concise syntax and have important this behaviour:

const square = number => number * number;
const add = (a, b) => a + b;
const describe = item => {
  const label = item.name.toUpperCase();
  return label;
};

An expression-body arrow function returns the expression implicitly. A block-body arrow function requires an explicit return.

Parentheses are optional for one parameter, but they are required for zero or multiple parameters. To return an object literal implicitly, wrap it in parentheses:

const makeUser = (name, role) => ({ name, role });

Arrow functions do not create their own this, arguments, or super. They capture this from the surrounding lexical context. This makes them useful as callbacks:

class Timer {
  start() {
    setTimeout(() => {
      console.log(this); // the Timer instance
    }, 1000);
  }
}

An arrow function is not automatically better than a traditional function. Use a traditional method or function when dynamic this, a constructor, or the arguments object is required.

3. Scope, Hoisting, and Closures

Scope determines where an identifier can be accessed. JavaScript has global scope, function scope, and block scope.

  • var is function-scoped and can be redeclared; it is generally avoided in modern code.
  • let and const are block-scoped.
  • A const binding cannot be reassigned, although an object or array held by that binding may still be mutable.
const settings = { theme: "light" };
settings.theme = "dark"; // allowed
// settings = {};         // not allowed

JavaScript uses lexical scope: a function can access variables from the location where it was defined, not from where it is called. The scope chain searches the current scope and then enclosing scopes.

Closures

A closure is a function together with access to variables in its surrounding lexical environment. The inner function may continue to use those variables after the outer function has finished:

function createCounter() {
  let count = 0;
  return () => {
    count += 1;
    return count;
  };
}

const next = createCounter();
next(); // 1
next(); // 2

The variable count is private to the closure. This supports encapsulation, factories, memoization, and event handlers. The retained environment also has a cost: unnecessary closures that remain referenced can keep data in memory. Event listeners and timers should be removed when no longer needed.

A common historical problem involved var in loops because it did not create block scope. Modern let creates a new binding for each loop iteration:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0, 1, 2

4. Strings and Text Processing

Strings are immutable sequences of UTF-16 code units. String methods return new strings rather than changing the original value.

const title = "  JavaScript Functions  ";
const clean = title.trim().toLowerCase();
const words = clean.split(" ");

Useful operations include:

OperationExamplePurpose
trim()text.trim()Removes leading and trailing whitespace
includes()text.includes("JS")Tests for a substring
startsWith()url.startsWith("https")Tests a prefix
slice()text.slice(0, 5)Extracts part of a string
replace()text.replace("old", "new")Replaces a match
split()text.split(",")Creates an array
join()items.join(", ")Creates a string from an array

Template literals use backticks and ${...} interpolation. Interpolated values should still be escaped or handled safely when inserted into HTML. Assigning untrusted text with innerHTML can create cross-site scripting vulnerabilities; use textContent for plain text unless HTML has been deliberately sanitized.

5. Arrays and Objects

An array is an ordered collection indexed from zero. It may contain mixed types, although consistent element types make programs easier to understand. An object is a collection of key-value properties and is often used to represent an entity.

const student = {
  id: 101,
  name: "Amina",
  skills: ["HTML", "CSS", "JavaScript"]
};

student.name;
student["name"];

Use dot notation for known, valid identifier-like property names and bracket notation for dynamic keys:

const field = "email";
const profile = { email: "[email protected]" };
console.log(profile[field]);

Objects and arrays are mutable. The spread syntax creates a shallow copy:

const updatedStudent = { ...student, name: "Amina Khan" };
const newSkills = [...student.skills, "Testing"];

A shallow copy does not clone nested objects. For deeply nested data, use a deliberate strategy such as structured cloning where supported, or design data updates to avoid unnecessary deep mutation.

Destructuring

Destructuring extracts values into variables:

const { name, id: studentId } = student;
const [firstSkill, secondSkill] = student.skills;

It can use defaults and rest syntax:

const { name = "Unknown", ...details } = student;
const [first, ...remaining] = student.skills;

Destructuring is especially useful for function parameters:

function displayStudent({ name, skills = [] }) {
  return `${name}: ${skills.join(", ")}`;
}

Optional chaining and nullish coalescing

Optional chaining prevents an error when an intermediate value is null or undefined:

const city = user.address?.city;

Nullish coalescing supplies a fallback only for null or undefined, unlike ||, which also treats values such as 0, false, and an empty string as false:

const pageSize = settings.pageSize ?? 20;

6. Common Array Methods

Array methods express common transformations more clearly than manual loops.

const products = [
  { name: "Keyboard", price: 40, active: true },
  { name: "Mouse", price: 25, active: false },
  { name: "Monitor", price: 180, active: true }
];

const activeNames = products
  .filter(product => product.active)
  .map(product => product.name);

Important methods include:

  • forEach(callback): performs an action for every element; it returns undefined.
  • map(callback): returns a new array containing one transformed value per element.
  • filter(callback): returns elements for which the callback is truthy.
  • find(callback): returns the first matching element or undefined.
  • findIndex(callback): returns the first matching index or -1.
  • some(callback): returns true if at least one element matches.
  • every(callback): returns true if all elements match; for an empty array, it returns true because no counterexample exists.
  • reduce(callback, initialValue): combines elements into one accumulated result.
  • sort(compareFunction): sorts in place and therefore mutates the array.

A reliable reduce() pattern supplies an initial value:

const prices = [10, 20, 5];
const total = prices.reduce((sum, price) => sum + price, 0); // 35

Without a suitable initial value, the first array element becomes the accumulator, which can cause type errors or unexpected results. The callback receives the accumulator, current value, index, and array. The accumulator may be a number, string, array, or object.

By default, sort() compares values as strings. Numeric sorting requires a comparator:

const scores = [9, 80, 12];
scores.sort((a, b) => a - b); // [9, 12, 80]

For non-mutating style, copy first: const ordered = [...scores].sort((a, b) => a - b);.

7. Program Structure and Modules

A well-structured program separates responsibilities. A user-interface module may render data, a validation module may check input, and a service module may communicate with an API. Each module should expose a small, intentional public interface.

Named and default exports

math.js:

export function average(values) {
  if (values.length === 0) return 0;
  return values.reduce((sum, value) => sum + value, 0) / values.length;
}

export const TAX_RATE = 0.2;

report.js:

import { average, TAX_RATE } from "./math.js";

export default function createReport(values) {
  return {
    average: average(values),
    taxRate: TAX_RATE
  };
}

Named exports are useful when a file provides several related capabilities. A default export represents the principal value of a module. Imports should use explicit paths, and browser modules are normally loaded with <script type="module" src="main.js"></script>.

Modules have their own scope, so variables are not automatically global. They are also loaded asynchronously by browsers and are subject to same-origin and server configuration rules. In professional applications, a bundler or build tool may combine modules, optimize assets, and provide development support, but the underlying import and export concepts remain the same.

Avoid circular dependencies when possible. If module A imports B and B imports A, initialization order can produce incomplete bindings or difficult-to-diagnose behaviour. A shared third module or clearer dependency direction is often better.

8. Worked Application: Course Progress Dashboard

The following illustrative case demonstrates functions, collections, immutability, destructuring, and modular design.

Background and problem

A training platform stores course records. It must display each learner's completed courses, calculate progress, and identify learners who need support. The initial code duplicates calculations in several event handlers and directly mutates records received from an API.

Analysis

The data consists of an array of learner objects. Each learner contains an array of course objects. Progress is derived data and should be calculated by pure functions rather than stored redundantly.

Approach

// progress.js
export const completionRate = courses => {
  if (courses.length === 0) return 0;
  const completed = courses.filter(course => course.completed).length;
  return completed / courses.length;
};

export const addProgress = learner => ({
  ...learner,
  progress: completionRate(learner.courses)
});

export const learnersNeedingSupport = (learners, threshold = 0.5) =>
  learners
    .map(addProgress)
    .filter(({ progress }) => progress < threshold);
// dashboard.js
import { addProgress, learnersNeedingSupport } from "./progress.js";

const learners = [
  {
    name: "Lina",
    courses: [
      { title: "HTML", completed: true },
      { title: "CSS", completed: true },
      { title: "JavaScript", completed: false }
    ]
  },
  {
    name: "Omar",
    courses: [
      { title: "HTML", completed: false },
      { title: "CSS", completed: false }
    ]
  }
];

const progressRecords = learners.map(addProgress);
const supportList = learnersNeedingSupport(learners);

for (const { name, progress } of progressRecords) {
  console.log(`${name}: ${Math.round(progress * 100)}%`);
}

Outcome

Lina receives a progress value of 2 ÷ 3, approximately 66.7%, while Omar receives 0 ÷ 2, or 0%. With a threshold of 0.5, only Omar appears on the support list. The functions do not change the original learner records; each enriched learner is a new object.

Learning points

The case illustrates separation of calculation from presentation, default parameters, array pipelines, destructuring, named exports, and a non-mutating update strategy. In a production system, input validation, tests, loading states, error handling, accessibility, and authorization would also be required.

9. Quality, Security, and Professional Practice

Readable program structure is a technical quality issue, not merely a matter of style. Useful practices include:

  1. Give functions one coherent responsibility.
  2. Use descriptive names and keep functions small enough to test.
  3. Prefer const; use let only when reassignment is necessary.
  4. Avoid hidden mutation, especially of data received from another module or API.
  5. Validate external input before processing it.
  6. Use textContent for untrusted text and avoid unsafe HTML construction.
  7. Handle empty arrays, missing properties, invalid numbers, and failed requests.
  8. Write unit tests for pure functions such as transformations and validators.
  9. Document exported functions, including expected inputs, outputs, and mutation behaviour.
  10. Keep module dependencies simple and directional.

AI-assisted coding tools can generate plausible functions, but generated code must be reviewed. Developers remain responsible for checking edge cases, security, licensing constraints, accessibility, and whether the code actually matches the application's data model. An AI suggestion that uses innerHTML, mutates shared state, or assumes trusted input may be unsuitable even if it appears to work in a demonstration.

10. Limitations and Common Errors

Several mistakes recur in beginner and professional code:

  • Confusing map() with forEach(): map() creates a result array; forEach() does not.
  • Forgetting that sort() mutates its receiver and compares as strings by default.
  • Calling a function instead of passing it as a callback: use button.addEventListener("click", handleClick), not handleClick().
  • Expecting const to make nested objects immutable.
  • Using || when 0 or false are valid values; use ?? when appropriate.
  • Returning an object from an arrow function without parentheses.
  • Relying on global variables, which creates hidden coupling.
  • Ignoring closure retention in long-lived event handlers.
  • Creating circular module dependencies.

Understanding these limitations helps developers select the right construct rather than applying a fashionable syntax mechanically.

Chapter Summary

Functions package reusable behaviour and may accept parameters, return values, and callback functions. Function declarations, expressions, and arrow functions differ in syntax and execution behaviour; arrow functions capture lexical this. Scope controls identifier visibility, while closures preserve access to surrounding variables and support encapsulation.

Strings are immutable and provide methods for searching, extracting, splitting, and formatting text. Arrays represent ordered collections and can be transformed with map(), filter(), reduce(), find(), some(), and every(). Objects model related properties, while destructuring, spread syntax, optional chaining, and nullish coalescing make data access and copying more expressive. Since copying is often shallow, developers must understand nested mutation.

ES modules provide file-level scope and explicit dependencies through export and import. Combining pure functions, predictable data transformations, careful validation, and well-designed modules produces software that is easier to test, secure, and maintain.

Key Terms

Function: A reusable unit of executable behaviour.

Parameter: A named input declared by a function.

Argument: A value supplied when a function is called.

Return value: The result sent from a function to its caller.

Callback: A function passed to another function for later execution.

Arrow function: A concise function syntax with lexical this.

Scope: The region in which an identifier can be accessed.

Block scope: Scope created by a pair of braces and used by let and const.

Closure: A function together with access to its lexical environment.

String: An immutable sequence of text characters represented by JavaScript string values.

Array: An ordered, index-based collection.

Object: A collection of key-value properties.

Destructuring: Syntax for extracting values from arrays or objects.

Spread syntax: Syntax that expands iterable elements or object properties.

Mutation: A change made to an existing object or array.

Pure function: A function whose result depends only on its inputs and which has no observable side effects.

Module: A file with its own scope and explicitly exported or imported values.

Named export: An explicitly named value exported from a module.

Default export: The principal exported value of a module.

Accumulator: The running result used by reduce().

Review Questions

Short Answer Questions

  1. What is the difference between a parameter and an argument?
  2. Why does an arrow function require an explicit return when its body uses braces?
  3. What is a closure, and how can it provide private state?
  4. What is the difference between map() and forEach()?
  5. Why might a developer use ?? instead of ||?

Long Answer Questions

  1. Explain JavaScript scope, including global, function, and block scope, and discuss why let and const are generally preferred to var.
  2. Compare function declarations, function expressions, and arrow functions. Include their syntax, hoisting considerations, and this behaviour.
  3. Explain how objects and arrays are copied with spread syntax, including the limitations of shallow copying.
  4. Describe how map(), filter(), and reduce() can be combined to process a collection. Give a practical example.
  5. Explain how ES modules improve program structure and identify risks associated with circular dependencies.

Analytical / Application Questions

  1. Design a function that receives an array of product objects and returns the names of active products costing less than 100. State whether your function mutates the input and justify your choice.
  2. A click handler reports the same final loop index for every button. Diagnose the likely scope problem and propose a modern JavaScript solution.
  3. Refactor a single large dashboard script into at least three modules. Define the responsibility and exported functions of each module, and explain how data should flow between them.

Multiple Choice Questions with Answers

Question 1

Which statement about a function's return value is correct?

A. Every function must return a string.

B. A function without an explicit return produces undefined.

C. The return statement displays the result automatically.

D. A return statement can only return numbers.

Correct Answer: B. A function without an explicit return produces undefined.

Question 2

What does this expression produce: const result = [1, 2, 3].map(n => n * 2);?

A. The original array is changed to [2, 4, 6].

B. The value is undefined.

C. A new array [2, 4, 6] is produced.

D. A single number 6 is produced.

Correct Answer: C. A new array [2, 4, 6] is produced.

Question 3

Which feature allows a returned function to retain access to a variable declared in its outer function?

A. Destructuring

B. Closure

C. Template literal

D. Module default export

Correct Answer: B. Closure

Question 4

Which statement about sort() is correct?

A. It always returns a new, non-mutated array.

B. It compares numbers numerically by default.

C. It mutates the array and compares values as strings by default.

D. It can only sort arrays of strings.

Correct Answer: C. It mutates the array and compares values as strings by default.

Question 5

What does user.address?.city do if user.address is undefined?

A. It throws a syntax error.

B. It returns undefined instead of attempting the property access.

C. It creates an empty address object.

D. It returns the string "undefined".

Correct Answer: B. It returns undefined instead of attempting the property access.

Question 6

Which syntax correctly creates a new object with all properties of user and an updated role?

A. { user, role: "admin" }

B. { ...user, role: "admin" }

C. [...user, role: "admin"]

D. user + { role: "admin" }

Correct Answer: B. { ...user, role: "admin" }

Question 7

Which array method returns the first element satisfying a condition, or undefined if none does?

A. filter()

B. some()

C. find()

D. every()

Correct Answer: C. find()

Question 8

What is the main purpose of export and import in JavaScript?

A. To convert strings into arrays

B. To define and use values across module files

C. To make all variables global

D. To prevent functions from returning values

Correct Answer: B. To define and use values across module files

Question 9

What does const total = [10, 20, 5].reduce((sum, value) => sum + value, 0); produce?

A. 5

B. 20

C. 30

D. 35

Correct Answer: D. 35

Question 10

Which statement accurately describes an arrow function?

A. It always creates its own dynamic this.

B. It cannot be assigned to a variable.

C. It captures this lexically from its surrounding context.

D. It must contain a block body.

Correct Answer: C. It captures this lexically from its surrounding context.

HTML, CSS & JavaScript
Chapter 10 of 15