DEV Community

Cover image for Scope and Closures in JavaScript
Thomas Finch
Thomas Finch

Posted on

Scope and Closures in JavaScript

Photo by Jan van der Wolf

Scope and closures are essential concepts that are applicable to every JavaScript code base. While most developers may have some familiarity with these concepts, they are fundamental topics that deserve dedicated attention.
Understanding scope and closures is crucial for writing efficient and maintainable JavaScript code. In this article we will be covering the following topics:

  1. The Scope
  2. Global Scope
  3. Function Scope
  4. Block Scope
  5. Closures

1. The Scope

Scope as a defined by a quick google search is as follows, “the extent of the area or subject matter that something deals with or to which it is relevant.”

In the context of JavaScript, scope refers to the extent of a variable's accessibility within our code. JavaScript encompasses three types of scope: Global, Function, and Block.

2. Global Scope

A variable with global scope is accessible globally throughout our code base. Any variable declared outside of a function with either the var, let or const keywords is said to have global scope.

let a = 1
const b = {someProperty: '2'};
var c = 'hello world';

function outer(){
    console.log(a);

    function inner(){
        console.log(b);   
    }
    inner();
}

console.log(c);

outer(); 

/*
Output:

1
{someProperty: '2'}
hello world

*/
Enter fullscreen mode Exit fullscreen mode

3. Function Scope

Every function has it’s own scope, therefore, a variable declared in a function is only accessible within that function scope therefore the variable has function scope. This applies to any variable declared with var, let and const.

function outer(){
    let a = 1
    const b = {someProperty: '2'};
    var c = 'hello world';

    function inner(){
        console.log(b);   
    }

    console.log(a);
    console.log(b);
    console.log(c);
    inner();
}

outer(); 
/*
Output:

1
{someProperty: '2'}
hello world
{someProperty: '2'}
*/

console.log(a);
/*
Output:

ReferenceError: a is not defined
*/
Enter fullscreen mode Exit fullscreen mode

As you can see in the final output, accessing the a variable outside of it’s functional scope is not possible and will result in a reference error.

Another interesting thing you can see here is that b in the outer function is accessible inside the nested inner function. This ability, where an inner function has access to the parent’s surrounding scope is known as the lexical or static scope.

We can nest functions as much as we like and each inner function will have access to the scope of all it’s outer functions.

Let’s look at one more example.

function outer(){
    const a = 1

    function inner(){
        const a = 'hello world'
        console.log(a);   
    }

    inner();
    console.log(a);
}

outer(); 
/*
Output:

hello world
1
*/

Enter fullscreen mode Exit fullscreen mode

In the above function we can see that although the inner function has access to the outer function’s scope, the variable a in it’s own scope with the same name takes precedence when accessing the variable of that name. This concept is known as variable shadowing or shadowing.

4. Block Scope

The block scope was introduced with the release of ES6 in 2015 and only applies to the two new variable types that were also introduced, const and let.

A block is considered as group of statements enclosed within curly braces. The statements typically consist of control flow statements e.g. if, else, switch, variable declarations and function calls.

Any variables declared with the variable type var will simply ignore the block scope and apply either function or global scope depending on the given lexical environment.

Let’s checkout an example demonstrating block scope.

const a = true;

if (a) {
  let b = 1;
  {
    const c = 2;
    var d = 3;
    console.log(b);
    console.log(c);
  }
  console.log(b);
}

console.log(d);

/*
Output:

1
2
1
3
*/
Enter fullscreen mode Exit fullscreen mode

As you can see block scope behaves identically to function scope except that it only applies to const and let variables, where the d variable declared with the var type simply ignores the block scope.

Additionally the block scope also adheres to the same shadowing and lexical scope behaviours we explored in function scopes.

5. Closures

MDN web docs defines a closure as “the combination of a function and the lexical environment within which that function was declared”.

Let’s take a look at an example which should provide a more intuitive understanding of this definition.


function outerFunction() {
  const outerVariable = 'I am the outer variable';

  function innerFunction() {
    console.log(outerVariable);
  }

  return innerFunction;
}

var closure = outerFunction();
closure();  

/*
Output:

I am the outer variable
*/
Enter fullscreen mode Exit fullscreen mode

The closure function in this example is the innerFunction(), it captures the scope of it’s parent function, which is the variable outerVariable, within it’s own scope.

The innerFunction() can then access the outerVariable at any time during execution even if not during execution of the parent function.

Putting it more simply, a closure is a function that simply remembers the lexical scope from the place where it was defined regardless of where or when it is executed later.

Let’s take a look at some real world use cases of closures.

5.1 Event handling

Here is an example of how keep we can keep track of the number of times a button is pressed.

// Assume we have an html button with id #button

function buttonSetup() {
    const buttonRef = document.getElementById("#button");
    let count = 0;

    buttonRef.addEventListener("click", function () {
      count++;
      console.log(count);
    });
}

buttonSetup();

/*
Button clicked:
1

Button clicked again:
2

Button clicked again:
3

*/
Enter fullscreen mode Exit fullscreen mode

As you can see here the anonymous event listener callback function captures the count variable in the buttonSetup() function in a closure.

Every time the button is clicked the count variable is simply incremented and its value logged to the console.

5.2 Private variables and encapsulation

In this example let’s model a toggle switch and hide access to it’s private internal state from the outside world.

function createToggleSwitch() {
  let toggleSwitch = "off";

  return {
    flickSwitch: function () {
      toggleSwitch = toggleSwitch === "off" ? "on" : "off";
    },

    logState: function () {
      console.log("state: ", toggleSwitch);
    }
  };
}

const toggle = createToggleSwitch();
toggle.logState();
toggle.flickSwitch();
toggle.logState();
toggle.flickSwitch();
toggle.flickSwitch();
toggle.logState();

/*
Output:

state:  off
state:  on
state:  on
*/
Enter fullscreen mode Exit fullscreen mode

As you can see the internal toggleSwitch variable is completely inaccessible to anything outside of the createToggleSwitch. However we can still manipulate the toggle switch state through accessor functions that have closures capturing the lexical scope of their parent function createToggleSwitch().

5.3 Currying

Currying is a closure pattern that transforms a function that takes multiple arguments into a sequence of functions, each taking a single argument. Each function captures the lexical scope of it’s parent.

This pattern allows us chain and compose multiple functions whilst increasing code reuse and improving readability.

In this example we want to use the process of currying to apply different discounts to a customer depending on the customer type.

const discountRules = {
  staff: 10,
  manager: 20,
  customer: 0
};

function calcDiscount(percentDiscount) {
  return function (purchasePrice) {
    return (purchasePrice * (100 - percentDiscount)) / 100;
  };
}

function createDiscount(customerType) {
  const discountAmount = discountRules[customerType];
  return calcDiscount(discountAmount);
}

const applyManagerDiscount = createDiscount("manager");
const applyStaffDiscount = createDiscount("staff");
const applyCustomerDiscount = createDiscount("customer");

console.log("manager final price: ", applyManagerDiscount(45));
console.log("staff final price: ", applyStaffDiscount(45));
console.log("customer final price: ", applyCustomerDiscount(45));

/*
Output:

manager final price:  36
staff final price:  40.5
customer final price:  45
*/
Enter fullscreen mode Exit fullscreen mode

In the above example there are two curried functions, createDiscount() and calcDiscount(). Both have distinct functionality where createDiscount() selects the percentage discount depending on the customer type and calcDiscount() applies the actual price discount transformation to the purchase price. By chaining these functions together we then create the specialised discount functions, applyManagerDiscount(), applyStaffDiscount() and applyCustomerDiscount().

It then becomes very intuitive to use and apply these specific discounts to the different customer types.

Another commonly confused but related concept to currying is ‘partial applications’. A partial application is the process of fixing or providing some arguments to a function, resulting in a new function with fewer parameters.

It is actually a more general concept where currying is a specialised type of partial applications, which takes multiple arguments and applies them to a sequence of functions each taking a single argument.

If you're interested in learning more about partial applications, a quick Google search will yield plenty of excellent content to explore.

6. Conclusion

There are 3 types of scope in JavaScript, Global, Function and Block.

Global scope means that a given variable is accessible throughout the code base. Function scope means that a given variable is only accessible within a function. Block scope, which only applies to const and let variables, means that a given variable can be accessed within a block i.e. a group of statements enclosed within curly braces.

The type of scope that applies to a given variable is based on the location or position of a given variable in the source code, this mechanism is known as the Lexical scope.

A closure allow us to capture a variable in a given lexical scope and reuse it in a different place from where it was defined regardless of where or when it is executed later.

There are many useful design patterns that employ closures this includes but is not limited to ‘event handling’, ‘encapsulation’ and ‘currying’.

Top comments (0)