JS - 03.06 - Return, Recursion, Call Stack

returning values, callback functions, recursion, and closures.

Returning a Value

Functions and Side Effects

Functions can serve two purposes: they can produce side effects (such as modifying a variable, printing to the console, or changing the DOM) or they can return values.

Functions that return values are more useful because their output can be reused in other parts of the program.

function sum(a, b) {
  return a + b;
}
let result = sum(1, 2);
alert(result); // 3

Here, sum returns a value (3) that is then stored in the variable result. This value can be used or passed around later.

Returning Early

The return statement immediately stops the function’s execution and optionally returns a value. If no value is returned, the function returns undefined by default.

function showMovie(age) {
  if (!checkAge(age)) {
    return;  // Early exit if age is not valid
  }
  alert("Showing movie");
}

Pure Functions

A pure function is one that:

  • Returns a value based only on its input arguments.
  • Has no side effects (does not modify global variables or interact with external state).

Pure functions are easy to test and reason about, as their output depends only on the input.


Functions as Values

In JavaScript, functions are first-class citizens. This means they can be:

  • Assigned to variables.
  • Passed as arguments to other functions.
  • Returned from other functions.

Functions as Values

A function can be treated as a value (like a string or a number), meaning you can assign it to a variable and execute it later.

function sayHi() {
  alert("Hello");
}

let func = sayHi;  // Assign function to variable
func();  // Executes sayHi() function
sayHi();

Regular values like strings or numbers represent the data. A function can be perceived as an action. We can pass it between variables and run when we want.

Passing Functions as Arguments

You can pass functions as arguments to other functions. These are often called callback functions.

function ask(question, yes, no) {
  if (confirm(question)) yes();  // If yes is clicked, call the `yes` callback
  else no();  // If no is clicked, call the `no` callback
}

function showOk() {
  alert("You agreed.");
}

function showCancel() {
  alert("You canceled.");
}

ask("Do you agree?", showOk, showCancel);

Here:

  • The ask function takes two callback functions (showOk and showCancel).
  • The appropriate callback is executed based on the user’s response.

Anonymous Functions as Callbacks

You can also pass anonymous functions directly as arguments to another function.

ask(
  "Do you agree?",
  function() { alert("You agreed."); },
  function() { alert("You canceled."); }
);

These anonymous functions are called immediately when the ask function executes. They are not assigned to any variables, so they exist only within the scope of the ask call.


The Call Stack

When a function is called, the program must “remember” where it left off in order to return to that point after the function finishes executing. This is handled by the call stack.

  • Every time a function is called, its execution context is pushed onto the stack.
  • When a function returns, its context is popped from the stack, and execution continues at the point where the function was called.
function chicken() {
  return egg();
}

function egg() {
  return chicken();
}

console.log(chicken() + " came first.");

This code causes infinite recursion. The functions chicken and egg keep calling each other, filling up the call stack until it overflows.

Stack Overflow

If the call stack grows too large (due to excessive function calls like the example above), a stack overflow error occurs. This happens when the computer runs out of space to store execution contexts.


Closures

A closure is a function that retains access to variables from its lexical scope, even after the function that created those variables has finished executing.

function wrapValue(n) {
  let local = n;
  return () => local;  // Returned function remembers the `local` variable
}

let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);

console.log(wrap1());  // 1
console.log(wrap2());  // 2

Here, the wrapValue function creates a closure. The returned function remembers the value of local from the scope in which it was created.

Closures are powerful because they allow a function to “remember” the environment in which it was created, even if that function is called later.

function multiplier(factor) {
  return number => number * factor;  // Closure capturing `factor`
}

let twice = multiplier(2);
console.log(twice(5));  // 10
  • The multiplier function creates a closure by returning a function that uses the factor parameter.
  • The twice function retains the environment where factor was set to 2.

Recursion

A function is recursive if it calls itself in order to solve a problem. Recursive functions are commonly used to solve problems that involve repetitive sub-problems (e.g., calculating factorials, traversing trees, etc.).

function power(base, exponent) {
  if (exponent === 0) {
    return 1;  // Base case: anything raised to the power of 0 is 1
  } else {
    return base * power(base, exponent - 1);  // Recursive case
  }
}

console.log(power(2, 3));  // 8

Solving a Problem with Recursion The problem asks for a sequence of operations (adding 5 or multiplying by 3) to reach a target number. Here’s how you could implement it recursively:

function findSolution(target) {
  function find(current, history) {
    if (current === target) {
      return history;  // Base case: return the history if we reach the target
    } else if (current > target) {
      return null;  // Stop if we exceed the target
    } else {
      // Recursively try adding 5 or multiplying by 3
      return find(current + 5, `${history} + 5`) || find(current * 3, `${history} * 3`);
    }
  }

  return find(1, "1");
}

console.log(findSolution(24));  // "(((1 * 3) + 5) * 3)"

Here, the function findSolution uses recursion to explore two possible operations at each step: adding 5 or multiplying by 3. It keeps track of the operations in the history variable.


Growing Functions

When working with larger functions, it’s best to break them down into smaller, reusable pieces.

Formatting Output To print the number of cows, chickens, and pigs on a farm, formatted with leading zeros.

function zeroPad(number, width) {
  let string = String(number);
  while (string.length < width) {
    string = "0" + string;
  }
  return string;
}

function printFarmInventory(cows, chickens, pigs) {
  console.log(`${zeroPad(cows, 3)} Cows`);
  console.log(`${zeroPad(chickens, 3)} Chickens`);
  console.log(`${zeroPad(pigs, 3)} Pigs`);
}

printFarmInventory(7, 16, 3);

Here:

  • zeroPad ensures each number is padded with zeros to be exactly three digits long.
  • printFarmInventory uses zeroPad to format and print each animal’s count.

  1. function min that returns the minimum of two numbers.
function min(a, b) {
  return a < b ? a : b;
}
  1. function countB that counts the number of B characters in a string.
function countB(str) {
  let count = 0;
  for (let char of str) {
    if (char === 'B' || char === 'b') count++;
  }
  return count;
}
  1. function countChar that counts occurrences of a specific character in a string.
function countChar(str, char) {
  let count = 0;
  for (let c of str) {
    if (c === char) count++;
  }
  return count;
}