6 min read

Understanding Functional Scope

Understanding Functional Scope

In JavaScript, functions determine the scope of a variable, leading to some unexpected scenarios that developers can come across while transitioning from block scope languages.

First off, there's the initial question of what is scope, exactly? Admittedly, I didn't know that the concept had a formal name until later on into my computer science education. The scope of something (be it a variable, function, etc) is the area of code that which it is accessible.

In many languages, such as C, your variables are scoped to the block of code that they are defined in.

Blocks

A block is a group of code that is semantically sectioned together. You may be familiar with blocks on a conceptual level, but not realize they had a formal definition!

An easy trick to identifying a block most time is to look for brackets.

int numberOfDonutsImCraving = 12;

if (numberOfDonutsImCraving > 4) { 
  // Block of Temptation
  // dude, no, you're on a diet...
} else {
  // Block of Perseverance
  // you should reward yourself with something, like a donut -- wait, no...
}

In the case above, we have a very simple scenario where we define the variable numberOfDonutsImCraving and check if it's got a value a greater than 0 or not.

There are actually 3 blocks here!

  1. The overall area that this code is contained in (global scope, inside a function, etc); we'll call this, for now, the Outside Block because it's fairly accurate and sounds like a cover band for Kids on the Block (kudos to a guy more clever than me for that one)
  2. The block inside of the if statement, if the condition is true; in the comments, we refer to this as the Block of Temptation
  3. The block inside of the else statement, if the condition is false; in the comments, we refer to this as the Block of Perseverance

Block Level Scoping

As mentioned above, we have a few blocks of code above. In languages like C, our scope is based on the current block. The nice thing about blocks is that they nest, allowing you to define blocks inside of blocks (like above!) that have access to variables and functions in a parent block. This concept of variables being accessible in a particular block of code is called block scope.

That means that you have a scope that looks like this:

  1. Outside Block
  2. Block of Temptation; can access Outside Block, as well
  3. Block of Perseverance; can access Outside Block, as well

In table format, it means:

Name Can Access Variables in Outside Block Can Access Variables in Block of Temptation Can Access Variables in Block of Perseverance
Outside Block Yes No No
Block of Temptation Yes Yes No
Block of Perseverance Yes No Yes

But what does that mean for me?

The concept of block scoping means you can do the following:

int numberOfDonutsImCraving = 12;

if (numberOfDonutsImCraving > 3) { 
  // Block of Temptation
  // dude, no, you're on a diet...
  // But if you're really craving this many, you probably can't resist
  int numberOfDonutsToActuallyEat = 0;

  if (numberOfDonutsImCraving > 6) {
    // eat two. go ahead, sabotage 4 years of progress.
    numberOfDonutsToActuallyEat = 2;
  } else {
    numberOfDonutsToActuallyEat = 1;
  }

  while (numberOfDonutsToActuallyEat > 0) { 
    eatOneDonut();
    numberOfDonutsToActuallyEat = numberOfDonutsToActuallyEat - 1;
  }
 
  numberOfDonutsImCraving = 0;
} else {
  // Block of Perseverance
  // you should reward yourself with something, like a donut -- wait, no...
  praiseSelf();
  eatSomeMacademiaNuts();
  cryOverTheHarshInjusticesInTheWorld();
}

There are a lot of little variables in that example.

The first thing you should take note of is that no matter where we are in the Block of Temptation or the Block of Perseverance we can access things in the Outside Block such as numberOfDonutsImCraving.

Since the Outside Block contains the other blocks, those child blocks can access any variables that have been defined up until that point in time.

The second thing you should know is that if you tried to access numberOfDonutsToActuallyEat in any block except for the Block of Temptation, then you would get a compiler error; it's simply not how things work. A child block can access variables in a parent block, but a parent block cannot access variables in a child block; naturally, you cannot access variables in a sibling block, either

Functions

Let's adapt that into JavaScript:

function praiseSelf() {
  console.log("I am have such self control; much wow, many happy.");
}

function eatOneDonut() {
  console.log("How can something so wrong taste so good?");
}

function eatSomeMacademiaNuts() {
  console.log("At least these are healthy...");
}

function cryOverTheHarshInjusticesInTheWorld() {
  console.log("How terrible the world is, that sugar is so hard on the body!");
}

function snackBecauseFrittataGetsAnnoyingAfterTwoWeeks() { 

  var numberOfDonutsImCraving = 2;

  if (numberOfDonutsImCraving > 3) { 
    // Block of Temptation
    // dude, no, you're on a diet...
    // But if you're really craving this many, you probably can't resist
    var numberOfDonutsToActuallyEat = 0;

    if (numberOfDonutsImCraving > 6) {
      // eat two. go ahead, sabotage 4 years of progress.
      numberOfDonutsToActuallyEat = 2;
    } else {
      numberOfDonutsToActuallyEat = 1;
    }

    while (numberOfDonutsToActuallyEat > 0) { 
      eatOneDonut();
      numberOfDonutsToActuallyEat = numberOfDonutsToActuallyEat - 1;
    }

    numberOfDonutsImCraving = 0;
  } else {
    // Block of Perseverance
    // you should reward yourself with something, like a donut -- wait, no...
    praiseSelf();
    eatSomeMacademiaNuts();
    cryOverTheHarshInjusticesInTheWorld();
  }
}

snackBecauseFrittataGetsAnnoyingAfterTwoWeeks();

You can run that in your browser to see some logs.

Hoisting

Something interesting happens in Javascript, however. Run these snippets of code in your Javascript console, and let's take a look at what happens:

Business as Usual

This looks pretty standard; test should log "Hello" and that is exactly what it does.

(function() { 
  var test = "Hello";
  console.log(test);
})();

Undefined Variable

This throws an error: ReferenceError: myTest is not defined which makes sense, given we never have a variable called myTest anywhere in the code.

(function() { 
  var test = "Hello";
  console.log(myTest);
})();

The Odd Case

You would expect this to error out in the same way as in the case of an undefined variable, but oddly enough it kind of works; it prints out undefined.

(function() { 
  console.log(test);  
  var test = "Hello";
})();

When Javascript is interpreted, it undergoes a process called hoisting. In this process, every variable in that function is quite literally hoisted to the top of the function declaration. That means that while our code has var test = "Hello"; as after the console log, Javascript interpreted the code as:

(function() { 
  var test;

  console.log(test);  
  test = "Hello";
})();

In which case, test is mostly definitely undefined. If we had multiple variables, they would each be placed in line as such:

(function() { 
  var test;
  var myTest;
  var myOtherVariable;
  var i;
  var j;
  var pizza;

  console.log(test);  
  test = "Hello";
})();

But why would it do that?

Functional Scoping

Javascript has what is known as functional scoping, which means that variables are accessible based on the function that which they are defined in. The Javascript interpreter scans each function and creates a list of variable and function names to define; I presume that has something to do with resource management, but I'll have to research on that!

This has some interesting consequences that make some bugs hard to find.

Case 1: Reused variable names

Take, for instance, the fact that variables defined in for loop declarations will be hoisted!

(function() {
  console.log("let's count from 0 to 10!")

  for (var i = 0; i <= 10; i++) {
    console.log("i is " + i + "/10");
  }

  console.log("let's count from 0 to 20!")
  for (var i; i <= 20; i++) {
    console.log("i is " + i + "/20");
  }
})();

In some languages, you would expect a syntax error to be thrown because, in the second loop, you're instantiating a variable without giving it a value. However, we know now that in Javascript, the code is essentially:

(function() {
  var i;
  console.log("let's count from 0 to 10!")

  for (i = 0; i <= 10; i++) {
    console.log("i is " + i + "/10");
  }

  console.log("let's count from 0 to 20!")
  for (i; i <= 20; i++) {
    console.log("i is " + i + "/20");
  }
})();

Case 2: Variables inside blocks

Let's rebuild that example, but change a few things. Let's build it out such that we compose a string and log one giant string:

(function() {
  console.log("let's count from 0 to 10!")
  var entireMessage = "";

  for (var i = 0; i <= 10; i++) {
    var currentMessage = "i is " + i + "/10\n";
    entireMessage = entireMessage + currentMessage;
  }

  console.log(entireMessage);
  console.log(currentMessage);
})();

If you'll notice, you're able to log the last currentMessage!

Knowing what we do about hoisting, we know that we have access to this variable because variables are hoisted to the top of the function declaration, meaning that we essentially wrote:

(function() {
  var entireMessage; 
  var currentMessage;

  console.log("let's count from 0 to 10!")
  entireMessage = "";

  for (var i = 0; i <= 10; i++) {
    currentMessage = "i is " + i + "/10\n";
    entireMessage = entireMessage + currentMessage;
  }

  console.log(entireMessage);
  console.log(currentMessage);
})();

This leads to some interesting side-effects, to say the least! We can easily create accidental clashes with variables names that way.

Let

In the modern world of Javascript, new ways of adding variables were introduced. One of these ways allows us to finally introduce block level scoping into Javascript!

We can use the let keyword in order to define a block level variable, which many programmers are rapidly adopting.

We can save ourself a world of headaches by writing this:

(function() {
  console.log("let's count from 0 to 10!")
  let entireMessage = "";

  for (let i = 0; i <= 10; i++) {
    let currentMessage = "i is " + i + "/10\n";
    entireMessage = entireMessage + currentMessage;
  }

  console.log(entireMessage); 
})();

Now, entireMessage is still available across the entire function because it is still in the top-level block.

However, i will only be defined for that for-loop, and each iteration of that for-loop will have its own block-scoped i. Not only that, butcurrentMessage will only be defined on each iteration of that loop, meaning it only exists in the scope that which it syntactically exists.


Currently Drinking: Guatemalan El Tambor from Toby's Estate, made in a Bodum Brazil 3 cup French Press. It's very tasty, light, and beginner friendly; heavy cream brings out a slightly hot-chocolate feel to it; absolutely delicious.