📣 Attention Salesforce Certified Trailblazers! Link your Trailhead and Webassessor accounts and maintain your credentials by December 14th. Learn more.
close
trailhead

Learn About Context, Scope, and Closures

Learning Objectives

After completing this unit, you’ll be able to:

  • Identify how variables are scoped in JavaScript.
  • Describe how this changes depending on where a function is called.
  • Use closures to capture references to variables in functions.

A critical piece of understanding any programming language is understanding the availability of variables, how state is maintained, and how to access that state. 

In JavaScript the availability and visibility of variables is referred to as scope. Scope is determined by where a variable is declared. 

Context is the state of the current execution of code. It is accessed through the this pointer. 

Variable Scope

Variables in JavaScript are declared using the var, let, or const keywords. Where you call the keyword dictates the scope of variable being created. 

Understanding the difference between these three comes down to two factors: assignment mutability and supporting nonfunction block scope. We covered assignment mutability in the first unit of this module. It’s time to discuss scope. 

Fun with Scope

The block of code where a variable or argument is declared determines its scope. But var does not recognise nonfunction blocks of code. This means calling var in an if block or a loop block will assign the variable to the scope of the nearest enclosing function. This feature is called hoisting

When using let or const, an argument’s or a variable’s scope is always the actual block in which it is declared. There is a classic thought exercise to show this.

function countToThree() {
  // i is in the scope of the countToThree function
  for (var i = 0; i < 3; i++){
    console.log(i); // iteration 1: 0
    // iteration 2: 1
    // iteration 3: 2
  }
  console.log(i); // What is this?
}

The console.log output inside the for loop is unsurprising, outputting the value of i for each iteration. What might be more surprising is the final console.log statement, which outputs 3. You might have expected an error, since i is declared inside what you would assume is scope of the for loop. But with hoisting, i actually belongs to countToThree’s scope. 

While not necessarily bad, hoisting is often misunderstood and can create variable leakage or cause accidental overwrites if a variable is redeclared in a code block. To address these misunderstandings let and const were added to the language to create variables with block-level scope. Let’s revisit the thought exercise.

for (let j = 0; j < 3; j++){
  console.log(j); // 0
  // 1
  // 2
}
console.log(j); // error

By substituting let for var, we now have a variable that exists only in the context of the for loop. Trying to access it after the loop has closed gives us an error. 

Context and this

As we explored, JavaScript revolves around objects. Objects are where state is tracked. When a function is invoked, there is always an object container around that function. This object container is its context and the this keyword points to that context. So context is not set when a function is declared, but rather where the function is invoked

Because functions can be passed around between objects, what this points to can change. 

Say, for instance, this JavaScript object.

var obj = {
  aValue: 0,
  increment: function(incrementBy) {
    this.aValue = this.aValue + incrementBy;
  }
}

If you then access the increment function, it works as expected.

obj.increment(2);
console.log(obj.aValue); // 2

But let’s assign that function to another variable and see how it works. 

//assign function to variable
var newIncrement = obj.increment;
//now invoke through the new pointer
newIncrement(2);
console.log(obj.aValue); // still 2 not 4

By assigning the variable to newIncrement, the function now is executed in a different context. Specifically, in this case, in the global context. 

Note

Note

The Function.apply(), Function.call(), and Function.bind() functions provide ways to invoke a function while explicitly binding it to a different object context.

The Global Object

When JavaScript is executed without any containing object that you write as a developer, it runs in a global object. For this reason, functions invoked there are said to be running in the global context, which means that accessing this will point there. 

In a browser, the global context is the window object. You can test this easily by running the following in your browser developer tools. 

this === window; // true

In the increment example, assigning the increment function to the newIncrement variable moves the context where it is invoked: to the global object. This is easy to demonstrate.

console.log(this.aValue); // NaN
console.log(window.aValue); // NaN
console.log(typeof window.aValue); // number

When we attempt to assign to this.aValue with the new context, the mutable nature of JavaScript objects comes into play. A new uninitialized aValue property is added to this. Performing math on an uninitialized variable fails, thus the NaN value. But we can see that aValue exists on window, and is indeed a number. 

Context with Your Objects

In the increment example, as long as the increment function is invoked using obj with the dot notation, this points to obj. Or, generally speaking, when calling a function as object.function() the thing to the left of the dot is always the context in which that function is invoked. 

Think about the Bike example. The Bike constructor defines several properties with the this reference. It also has functions assigned to its prototype that reference this

const Bike = function(frontIndex, rearIndex){
  this.frontGearIndex = frontIndex || 0;
  this.rearGearIndex = rearIndex || 0;
  ...
}
...
Bike.prototype.calculateGearRatio = function(){
  let front = this.transmission.frontGearTeeth[this.frontGearIndex],
  rear = this.transmission.rearGearTeeth[this.rearGearIndex];
  if (front && rear) {
    return (front / rear) ;
  } else {
    return 0;
  }
};

We then call Bike with the new keyword. 

const bike = new Bike(1,2);
console.log(bike.frontGearIndex); // 1
console.log(bike.rearGearIndex); // 2

This looks like we’re invoking the Bike constructor in the global context. However the new keyword shifts the context (and the this pointer) to the new object on the left side of the assignment. 

When we invoke any of the functions they are now members of the bike object, so they use that as the containing context. 

let gearRatio = bike.calculateGearRatio();
console.log(gearRatio); // 3

It’s easy to invoke a constructor the wrong way. Here’s where things can fall apart.

const badBike = Bike(1,2);
console.log(badBike.frontGearIndex); // error
console.log(window.frontGearIndex); // 1

When you forget to use new, Bike is called like any other function and that crucial shift of this from window to the newly created object fails. Object mutability steps in and the frontGearIndex property is added to window instead. 

Note

Note

The class syntax in JavaScript forces you to invoke a constructor with the new keyword, so you can’t misdirect your context.

Closures

When a function is declared, it holds a reference to any variables or arguments declared within it, and any variables it references in the scope that it is contained within. This combination of its variables and arguments along with local variables and arguments from its containing scope is called a closure. 

Consider this function, and the function it returns. 

const greetingMaker = function(greeting){
  return function(whoGreeting){
    return greeting + ", " + whoGreeting + "!";
  }
}
const greetingHello = greetingMaker("Hello");
const greetingBonjour = greetingMaker("Bonjour");
const greetingCiao = greetingMaker("Ciao");
console.log(greetingHello("Gemma")); // Hello, Gemma!
console.log(greetingBonjour("Fabien")); // Bonjour, Fabien!
console.log(greetingCiao("Emanuela")); // Ciao, Emanuela!

When greetingMaker is invoked, we might normally imagine its greeting argument to last only for the life of it being called. 

But the returned function keeps a reference to the greeting argument in greetingMaker’s scope. So that finally when it is invoked through greetingHello/Bonjour/Ciao, it’s still accessible. 

Grasping closures is also an essential part of understanding and using the language. 

Resources 

Code Sample Project for This Module

JavaScript Closures

Scope and Closures

retargeting