JS: Execution Contexts & Scope

Learning Goals

  • Understand the order of execution for JavaScript code and why it matters
  • Explain what an execution context is and describe what happens in both of its phases
  • Describe the differences between var, let and const and when to use each
  • Understand how the scope chain is initialized and utilized to resolve variables

Vocab

  • JavaScript Engine/Interpreter A program that executes JavaScript code. Most commonly used in web browsers
  • Hoisting The process of implicitly moving the declaration of variables and functions to the top of their scope
  • Creation Phase The phase where the interpreter sets aside some space in memory to store any variables and functions we might need access to.
  • Execution Phase The phase where the interpreter executes Javascript code, line-by-line.
  • Execution Call Stack A tool (data structure) for the interpreter to keep track of its place in a script that calls multiple functions
  • Scope - The place in which a variable can be accessed.
  • Scope Chain - A stack of currently accessible scopes, from the most immediate context to the global context.

How JavaScript is Read

Having a good grasp on what is going on ‘under the hood’ - particularly how JavaScript is read by the browser - makes writing solid JavaScript code, debugging, and self-teaching new concepts much easier.

Each browser has what’s called a ‘JavaScript Engine’ that translates (or interprets) your code line by line as it executes, which allows your application to perform the behaviors and interactions you’ve programmed. For example, if you’ve written the following code:

const header = document.getElementById('header');
header.innerText = 'Lorem Ipsum Dolor'

The JavaScript engine will read and interpret these two lines in the order they’ve been written:

  • first the browser will find the header element,
  • then it will update its inner text.

You can think of the JavaScript engine as a foreign language translator, who acts as an intermediary between two people who don’t speak the same language. As developers, we understand how to write JavaScript, the JavaScript Engine knows how to read JavaScript, and can pass those instructions onto the rest of the browser.

Understanding the Order of Execution

Just like we might read a book, we must completely finish reading one line before we move onto the next (otherwise that book wouldn’t make much sense to us)! In programming languages, this is what we call single-threaded.

JavaScript is a single-threaded language, which means each line of code must fully finish executing before it can move onto the next – only one task can be executed at a time.

In Your Notebook

var modTwoTeachers = ['Hannah', 'Nik', 'Leta'];

function calculateEvals (teachers, classSize) {
  return classSize / teachers.length;
}

var numEvals = calculateEvals(modTwoTeachers, currentCohort);

var currentCohort = 33;
console.log(numEvals);

Looking at the example above, what would you expect to be logged when we get to line 10? Why?

Let’s do a quick breakdown of what the interpreter did here to read this code:

Looking at another example

const moo = mooLikeACow();

function mooLikeACow() {
  return 'Moooo!';
}

console.log('Animal Sound: ', moo);

What would we expect to be logged when line 7 executes? Why? Is the actual behavior different than you expected?

Hoisting & The Creation Phase

In order to understand what’s happening here, we must explore another step the interpreter takes before executing our code.

The interpreter takes a first pass to skim over our code and get a general idea of what we’re doing and what variables and functions we’ll be using. This is called the creation phase. In the creation phase, the interpreter sets aside some space in memory to store any variables and functions we might need access to.

Using the first code example, the interpreter recognizes that we’re going to be working with a function called calculateEvals and some variables - modTwoTeachers, numEvals, and currentCohort. In trying to be helpful, the interpreter hoists these functions and variables to the top of our scope. Behind the scenes, the interpreter would essentially be changing our code to look something like this:

var modTwoTeachers, numEvals, currentCohort;
function calculateEvals(teachers, classSize) {
  return classSize / teachers.length;
}

modTwoTeachers = ['Hannah', 'Nik', 'Leta'];

numEvals = calculateEvals(modTwoTeachers, currentCohort);

currentCohort = 33;
console.log(numEvals);

Our variable declarations are hoisted to the top of our code block, but their initialization or assignment remains on the original line it was written. Therefore, all three of our variables are undefined until the execution phase when the interpreter reaches the lines where we assign them values.

Our function is also hoisted to the top of our code block, with its entire definition alongside it. This gives us insight into why our second example still worked without throwing an error:

function mooLikeACow() {
  return 'Moooo!';
}

const moo = mooLikeACow();

console.log('Animal Sound: ', moo);

When functions are hoisted to the top of our code block, it hoists not just the function name, but the code inside of it as well. This means we can invoke functions before we’ve declared them without running into errors.

This hoisting behavior adds some complexity to the JavaScript language, and is important to understand thoroughly in order to anticipate the values of your variables at any given time.

Turn and Talk

With a partner, take turns explaining how the following JavaScript code would be translated by the interpreter. We will come back together as a class to discuss:

const hungriestDog = 'Tess';

function begForTreats(seconds) {
  const result = seconds * 2;

  if (result > 5) {
    return 'This human is rude, not giving me treats. Onto the next one.';
  } else {
    return 'Yum, human food!';
  }
}

let beggingTime = 1;

let beg = begForTreats(beggingTime);

beggingTime = 4;
console.log(beg)

Execution Call Stack

Chances are good that you have come across information that references The stack, The call stack, or the Execution Call Stack. A call stack is a way for the JavaScript interpreter to keep track of its place (its current execution context) in a script that calls multiple functions — what function is currently being run, what functions are called from within that function and should be called next, etc. A stack is a fundamental data structure in Computer Science that follows “First-In-Last-Out” (FILO) semantics. Any time a new function is invoked (and a new execution context is created) this execution context is pushed to the stack. Once a function has returned, the call is popped off the stack. The stack is used to determine in what order the code runs. Let’s look at this example:

function buildLaser () {  
  var message = 'Laser Built';  
  console.log(message);
}

function buildMoonBase () {  
  var message = 'Moon Base Built';  
  buildLaser();
  console.log(message);
}

function ransomTheWorld () {
  buildMoonBase();  
}

ransomTheWorld();  

Scope

Now that we understand the order of execution a bit, we can dive deeper into the concept of scope. Scope is how we describe where a variable or value can be accessed.

Pre-ES6 variables often are described as having either global scope or local (function) scope.

  • Looking at the below example, does our makeNoise function have access to the cowNoise and catNoise variables?
  • What about outside of our function? Do we have access to cowNoise and catNoise here as well?
  • How would you describe the differences between a globally vs locally scoped variable?
var cowNoise = 'moo';

function makeNoise() {
  var catNoise = 'meow';
  console.log('Cow Noise inside of Function: ', cowNoise);
  console.log('Cat Noise inside of Function: ', catNoise);
}

makeNoise();

console.log('Cow Noise outside of Function: ', cowNoise);
console.log('Cat Noise outside of Function: ', catNoise);

Global, Function/Local, and Block Scope

We have several scopes available to us: global, local (also known as function), and block scope.

Global scope

Here is an example that uses globally scoped variables. They’re declared at the top level of our file, not tucked inside a function or other block of code.

var one = "one";
let two = "two";
const three = "three";

showNumbers();

function showNumbers () {
  console.log("In function: ", one, two, three);
  if (one && two && three) {
    console.log("In if block: ", one, two, three);
  }
}

Function/Local Scope

Here is an example that uses locally (or function) scoped variables. We can see that they are declared within a function.

function readWords() {
  var greeting = "Hello, friend, ";
  let question = "how are you? ";
  const response = "I am fine."

  if (true) {
    console.log('Sentence in the if block: ', greeting, question, response);
  }

  console.log(greeting + question + response);
}

readWords();

// Am I able to access the variables here?
console.log(greeting + question + response);

Block Scope

Here is an example that uses block scoped variables. We can see that they are declared with in the if block of code.

function readWords() {
  var greeting = "Hello, friend, ";
  let question = "how are you? ";
  const response = "I am fine."

  if (true) {
     var greeting = "Sup dawg, ";
     let question = "what's good?";
     const response = "Nm."
     console.log('Sentence in if block: ', greeting, question, response);
  }

  console.log(greeting + question + response);
}

readWords();

Scope & Scope Execution Practice

Parent vs. Child Scopes

Let’s look at another example and compare how scopes work between the parent and child.

Problem #1

Review the example below and answer the following questions:

const array = [5, 4, 3, 2, 1];
const secondNumber = array[1];

function getFirstNumber() {
  const firstNumber = array[0];
  return firstNumber;
}

function getSecondNumber() {
  return secondNumber;
}

console.log('getFirstNumber(): ', getFirstNumber())
// console.log('getSecondNumber(): ', getSecondNumber())

// console.log('secondNumber: ', secondNumber);
// console.log('firstNumber: ', firstNumber);
  • Run the getFirstNumber either in a sandbox or your console. What happens & why?
  • Do the same for getSecondNumber and discuss similarly what happens & why.
  • Finally, log secondNumber and firstNumber. Note what happens when doing one vs the other. Why?

Review Note:

Parent scopes do not have access to child scopes BUT child scopes do have access to their parent scope.

Block Scoped Variable Practice

As we discussed earlier, variables declared with the keyword let or const will be block scoped if declared within a block. This means that they are scoped to the block statement (if, for…) in which they are declared. When you see { and }, those curly brackets are likely creating a scope, - as with function, if, and for.

Problem #2

Run the following examples in your sandbox or console:

// Example #1:
let message = 'You are doing great!';

if (message.length > 0) {
  let message = 'I think you are amazing!';

  console.log('Inside of conditional:', message);
}

console.log('Outside of conditional:', message);
// Example #2
function getIndex(searchQuery) {
  const names = ["Nik", "Travis", "Hannah"];

  for (let i = 0; i < names.length; i++) {
    if (names[i] === searchQuery) {
      console.log ('The index is: ', i);
      break; //break just stops the for loop execution
    }
  }
  return i;
}

console.log('getIndex(): ', getIndex("Hannah")); // What will happen?
  • Run this code in a sandbox or console. What happens?
  • Be prepared to try to explain what is happening and why. Guesses are fine!
  • Then replace let with var in Example #2 and note what happens!

In Your Notebook

  • Describe “scope” in your own words.
  • What are the similarities and differences between var, let, and const?
  • What might be a metaphor or analogy for scope? Draw or diagram it out.

Scope Chain

Whenever a variable is used, the JavaScript interpreter traverses the scope chain until it finds an entry for that variable. Traversal on the scope chain always starts in the most immediate scope and moves towards the global space. Note that the scope chain is initialized during the “creation phase” of the interpreter running through the code. Let’s see an example of this in action!

In Breakout Groups

Consider the following example and explain what is happening:

let number = 10;

function logNumber() {
  number = 20;
  console.log('A', number);
}

console.log('B', number);

logNumber()

console.log('C', number);
  • Before running the code, what do you think the value of number is in each of the logs?
  • Now run it and take note of what happens. Allow each person in the group to explain what they think is happening.

Major Takeaways

  • The scope chain (e.g. “What is the parent scope for this variable? The grandparent scope?”) is determined by where functions are defined in the code base…. not where functions are invoked.

  • Every time a variable is initialized, the interpreter will first look in its own scope to see if the label can be found. If it is not found, it will look “up” the scope chain to the parent scope to try to resolve the variable in the parent context.

  • If that label is never found, the interpreter will declare it globally on the window and the variable will be scoped as such.

Clarification between Scope Chain & Hoisting

It is important to note that the interpreter moving up the scope chain to resolve variable values is NOT hoisting. Remember that the JS interpreter hoists declarations (storing them in memory) during the creation phase, not when the code itself is being executed.

Final Reflections

Using your journal, take a few minutes to answer the following:

  • What is hoisting?
  • What is an execution context?
  • Why is it important to understand scope?
  • What is the scope chain? What does it do?

Additional Resources

Lesson Search Results

Showing top 10 results