Understanding JavaScript Scopes & Closures

Understanding JavaScript Scopes & Closures

Let's begin by exploring the concept of scopes: what exactly are scopes?

Scope is the current context of execution in which values and expressions which are accessible or can be referenced are available for use.

JavaScript has four types of scopes:

Block Scope

Block scope, created with curly braces {}, is specifically associated with variables declared using "let" and "const". Variables declared inside blocks with "let" and "const" are accessible only inside the block where they are declared and cannot be accessed from outside the block. On the other hand, variables declared with "var", functions declared inside blocks or any other declarations may have different accessibility depending on the block of code execution and the behavior of hoisting in the JavaScript engine.

Function Scope

Every function in JavaScript has its own scope meaning anything declared inside function is not accessible outside the function. Everything declared inside a function has local scope.

Example :

function orderPizza() {
    if (true) {
        var crustType = 'thin';
        let toppings = ['cheese', 'pepperoni'];
        const sauce 'tomato';

        function preparePizza() {
            console.log('Preparing pizza with', crustType, 'crust,', toppings.join(', '), 'toppings', and, sauce, 'sauce.');
        }
        preparePizza(); // Preparing pizza with thin crust, cheese, pepperoni toppings, and tomato sauce.

    } else {
        function apologizeForNoPizza() {
            console.log('Sorry, we are out of pizza options today.');
        }
    }

    // Attempting to access variables and functions declared inside the block
    console.log('CrustType:', crustType); // CrustType: thin
    // console.log('Toppings, toppings); // ReferenceError: toppings is not defined 
    // console.log('Sauce', sauce); // ReferenceError: sauce is not defined

    preparePizza(); // Preparing pizza with thin crust, cheese, pepperoni toppings, and tomato sauce.
    // apologizeForNoPizza(); // TypeError: apologizeForNoPizza is not a function
}

orderPizza();
// console.log('CrustType:', crustType); // ReferenceError: crustType is not defined 
// preparePizza(); // ReferenceError: preparePizza is not defined

Explanation:

  • Block Scoped Variables And Function: Inside the if (true) block, toppings and sauce are block-scoped variables.

  • Function Scoped Variables And Function: Inside the if (true) block, crustType and preparePizza is a function scoped variable and function for orderPizza. Inside the else block apologizeForNoPizza is a function scoped variable and function for orderPizza.

  • Function Scoped Variables And Function are hoisted at top and during compilation phase and while runtime initialization takes place.

  • if (true) block is executed during runtime preparePizza and crustType are initialized and is accessible throughout function orderPizza while else block is not executed therefore apologizeForNoPizza is not being initialized and inaccessible throughout function orderPizza.

  • Toppings and sauce are block scope and as per block scope they cannot be accessed outside block.

  • When we access any orderPizza function scope declarations outside orderPizza, it will throw error because as per function scope any declaration is accessible only inside the function where it is declared.

Global Scope

Variables which are accessible from anywhere in application are global scope.

When there is no module system in place it is easier to create global variables which are easily accessible in any file of that application.

Consider an HTML file that imports two JavaScript files: file1.js and file2.js:

< script src = "filel.js" > < /script> 
< script src = "file2.js" > < /script>

//Contents of file:
// filel.js
function hello() {
    var localMessage = 'Hello!';
}
var globalMessage = 'Hey there!';

// file2.js
console.log(localMessage); // localMessage is not defined becuase it has function scope and is available locally inside the function hello
console.log(globalMessage); // Hey there!

In browsers, the window object represents the global scope. Any variable or function declared directly in the global scope becomes a property of the window object. Therefore, you can access global variables and functions using window.variableName or window.functionName.

Example :

window.name = 'John';
window.getName = function() {
    return window.name;
};

It is advisable to use global variables only when it needs to be only accessed globally and avoid overwriting global variables because it can cause unintended side effects.

Module Scope

If you create a variable within a JavaScript module but outside of a function or block, it doesn't have global scope, but rather module scope. To make a module-scoped variable available to other files, you must export it from the module where it's created and then import it from the module that needs to access the variable.

Example :

// hello.js file
function hello() {
    return 'Hello world!';
}
export { hello };

// app.js file 
import { hello } from './hello.js';
console.log(hello()); // Hello world!

Exploring the depths of JavaScript's scope landscape, we've traversed through all four scopes. Now, let's unlock the captivating world of closures.

Closures

A closure is like a special package: it includes a function and all the variables it needs from the area around it. So, when you have a function inside another function, the inner one can still use the variables from the outer one. In JavaScript, every time you create a function, you also create a closure.

Now, think of closures like having three layers:

1. The innermost layer is the function's own set of variables, called the local scope.

2. Around that, there's the scope of the function that contains it. This can be either a block of code, another function, or even the whole module.

3. Lastly, there's the outermost layer, which is the global scope, containing variables accessible throughout your entire code.

Example :

function createCricketPlayer() {
    // Private Variables 
    let totalRuns = 0,
        totalBallsPlayed = 0,
        totalWickets = 0,
        totalBallsBowled = 0,
        strikeRate = 0.0,
        economyRate = 0.0;

    // Public Method
    function updateStats(runsScored, ballsPlayed, wicketsTaken, ballsBowled) {
        totalRuns += runsScored;
        totalBallsPlayed += ballsPlayed;
        totalWickets += wicketsTaken;
        totalBallsBowled += ballsBowled;
        strikeRate = calculateStrikeRate();
        economyRate = calculateEconomyRate();
    }

    // Public Method
    function getPlayerStats() {
        return {
            totalRuns,
            totalBallsPlayed,
            totalWickets,
            totalBallsBowled,
            strikeRate,
            economyRate
        };
    }

    // Private method to calculate strike rate
    function calculateStrikeRate() {
        if (totalBallsPlayed === 0) {
            return 0.0; // To avoid division by zero error
        }

        return (totalRuns / totalBallsPlayed) * 100;
    }

    // Private method to calculate economy rate 
    function calculate EconomyRate() {
        if (totalWickets === 0) {
            return 0.0; // To avoid division by zero error
        }

        return totalBallsBowled / totalWickets;
    }

    return {
        updateStats,
        getPlayerStats
    };
}

// Create cricket players
const player1 = createCricketPlayer();
const player2 = createCricketPlayer();

// Simulate matches and update player1 stats 
player1.updateStats(50, 30, 1, 120);
player1.updateStats(30, 20, 0, 100);
player1.updateStats(20, 15, 2, 80);

// Simulate matches and update player2 stats 
player2.updateStats(40, 25, 2, 110);
player2.updateStats(35, 22, 1, 90);
player2.updateStats(25, 18, 1, 75);

// Get playerl's current stats
const player1Stats = player1.getPlayerStats();
console.log("Player 1 Stats:", player1Stats);
/*
Player 1 Stats: {
  totalRuns: 100,
  totalBallsPlayed: 65,
  totalWickets: 3,
  totalBallsBowled: 300,
  strikeRate: 153.84615384615387,
  economyRate: 100
*/

// Get player2's current stats
const player2Stats = player2.getPlayerStats();
console.log("Player 2 Stats:", player2Stats);
/*
Player 2 Stats: {
  totalRuns: 100,
  totalBallsPlayed: 65,
  totalWickets: 4,
  totalBallsBowled: 275,
  strikeRate: 153.84615384615387,
  economyRate: 68.75
}

From the above cricket player example, we can see how closures in JavaScript mirrors the principles of object-oriented programming (OOP), particularly in terms of encapsulation and data hiding. Before the introduction of classes in JavaScript, developers relied on closures to achieve encapsulation and emulate private methods.

A common mistake in JavaScript programming is overlooking the fact that a nested function can access not only its immediate outer function's variables but also variables from any surrounding functions. This creates a chain of function scopes that the nested function can tap into, which may lead to unexpected behavior if not properly understood and managed.

Example :

...
// global scope
const e = 10;

function sum(a) {
    return function(b) {
        return function(c) {
            // outer functions scope
            return function(d) {
                // local scope
                return a + b + c + d + e;
            };
        };
    };
}

console.log(sum(1)(2)(3)(4)); // 20

In the above example, each nested function has access to variables from its enclosing scopes. Let's break down the chain of scopes:

  • Local Scope (d): This is the innermost function's scope, where the parameter d is defined.

  • Outer Function Scope (a, b, c, e): The outer function has access to its parameters a, b, and c, as well as the global variable e.

  • Outermost Function Scope (sum function): The sum function's scope is the outermost enclosing scope. It doesn't have any local variables, but it has access to the parameters passed to it a, and since it's defined in the global scope, it also has access to the global variable e.

  • Global Scope (e): This is the outermost scope, containing the global variable e.

So, when the innermost function is executed, it can access variables a, b, c, and e from its enclosing scopes, all the way up to the global scope. This demonstrates how JavaScript's scoping allows functions to access variables defined in their outer scopes, forming a chain of scopes.

I hope you found this exploration of scopes and closures insightful and empowering! By understanding these fundamental concepts and their practical applications, you're better equipped to write robust and efficient JavaScript code. Whether you're a beginner or an experienced developer, mastering scopes and closures opens up a world of possibilities in your coding journey.

Happy learning and building!