DEV Community

Tetsuo TANAKA
Tetsuo TANAKA

Posted on

What Really Happens When You Call a Function? Watching the Call Stack

In this series, we use JSVisualizer, a free browser-based tool, to understand classic algorithms by watching them run. Last time we looked at conditionals.

This time the topic is functions, and the star of the show is the Call Stack view.

Where beginners get stuck

This program adds the squares of two numbers. 3 * 3 + 4 * 4 is 25.

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

function sumOfSquares(x, y) {
  return square(x) + square(y);
}

let x = 10;
let answer = sumOfSquares(3, 4);
console.log(answer);
console.log(x);
Enter fullscreen mode Exit fullscreen mode

What does the last line, console.log(x), print? People who've just met functions often wonder:

  • x shows up in three places. It's a parameter (a variable that receives a value passed to a function) of square, a parameter of sumOfSquares, and the let x outside. Are these the same thing or different things?
  • Where does execution go when you call a function, and how does it get back? Partway through square(x) + square(y), the program jumps into another function. How does it know where to pick up again?
  • If a function changes a value, does the change reach the outside?

JSVisualizer's Call Stack view shows you exactly this: each function call gets its own place for variables, which appears when the call starts and vanishes when it ends.

Seeing it in the Call Stack view

How to read it

Paste the code into JSVisualizer, press ▶ Run, and open the Call Stack tab. Then press ▷ Human (one "human-friendly" step) three times:

Call Stack view: Global holds x = 10 and more; below it, sumOfSquares(3, 4) holds x = 3 and y = 4

Each box is called a frame: a place for variables that belongs to one particular function call.

  • Global holds the variables outside any function, and it's always at the top. square = ƒ square means there's a function named square, and answer = — means answer doesn't have a value yet.
  • sumOfSquares(3, 4) is the frame for the call we just made. Its heading shows the values that were passed in.
  • The blue outline marks the frame that's running right now.

The sumOfSquares frame has x = 3, while Global has x = 10. Same name, but they live in different places: they're two separate variables.

Calls pile up

Press ▷ Human six more times (nine in total), and look at the whole screen:

Full screen: on the left, line 2 (return x * x) is running and square(y) on line 6 has a dashed purple underline; on the right, Global has x = 10, square(4) has x = 4, and sumOfSquares(3, 4) has x = 3 and y = 4

  • On the left, line 2 inside square is running, and the call that got us here, square(y), has a dashed purple underline.
  • On the right, a square(4) frame has appeared under Global, pushing sumOfSquares(3, 4) down. New frames always appear right below Global.

Now look at x: it's 10 in Global, 4 in square(4), and 3 in sumOfSquares(3, 4). Three variables with the same name exist at the same time, with three different values.

Frames vanish

Press ▷ Human five more times (14 in total):

Call Stack view: only Global remains, with x = 10 and answer = 25

All the function frames are gone and only Global remains. answer now holds 25. The x = 3 and x = 4 we saw earlier disappeared with their frames, and Global's x is still 10. That's what the final console.log(x) prints.

What the frames reveal

1. Same name, different frame, different variable

A variable exists only in the frame where it was created. When square says x, it means the x in square's frame. The region of code where a variable can be used is called its scope.

This is why you can write a function without worrying about what variable names its callers happen to use.

2. The newest call finishes first

Step through and watch the frames:

  1. The sumOfSquares(3, 4) frame appears
  2. The square(3) frame appears, then vanishes when it finishes
  3. The square(4) frame appears, then vanishes when it finishes
  4. The sumOfSquares(3, 4) frame vanishes

The most recent frame is always the first to go, just like taking plates off the top of a pile. A pile that works this way is called a stack, and since this one is made of function calls, it's the call stack.

Notice that the sumOfSquares frame stays put while square runs. That's how execution can pick up where it left off in square(x) + square(y).

3. Changing a parameter doesn't change the variable outside

What does this program print?

function addOne(n) {
  n = n + 1;
  return n;
}

let n = 10;
addOne(n);
console.log(n);
Enter fullscreen mode Exit fullscreen mode

Not 11. Pausing inside addOne shows why:

Call Stack view: Global has n = 10, and addOne(10) has n = 11

The n in the addOne(10) frame has become 11, but Global's n is still 10. A function doesn't receive the variable itself. It receives a copy of its value. (Arrays and objects work a little differently; we'll get to that later in the series.)

Call Stack view: the addOne frame is gone, only Global with n = 10 remains, and the console shows 10

When the function ends, its frame vanishes, and the 11 goes with it, because nothing used the value that return n handed back. Write n = addOne(n); and the returned 11 is stored in Global's n.

Try it yourself

Open the link below and press ▶ Run; the Call Stack view opens. Step forward with ▷ Human (the h key) and back with ◁ Human (H).

▶ Open in JSVisualizer (sum of squares, Call Stack view)

To change the code, press ✏ Edit. Before each run, predict how the frames will behave:

  • Change sumOfSquares(3, 4) to sumOfSquares(square(2), 4). Which frame appears first?
  • Add console.log(y); at the very end. What happens, and why?

One more program to play with:

Wrapping up

  • Calling a function creates a frame, a place for variables that belongs to that call. When the call ends, the frame vanishes.
  • Variables with the same name in different frames are different variables. The Call Stack view shows them side by side, with different values.
  • The newest frame is the first to go. A function receives a copy of a value, so changing a parameter inside doesn't change the variable outside.

Next up: how expressions are evaluated. We'll use the Expr view to see the order in which a complicated expression like a + b * c > 10 && d gets worked out.

Top comments (0)