Main menu

Pages

JavaScript variable visibility: demystifying scope and closures for a comprehensive understanding

1. Introduction

Scope and closures are fundamental concepts in JavaScript that play a crucial role in understanding how variables and functions are accessed and managed within a program. Having a solid understanding of scope and closures empowers developers to write efficient, modular, and maintainable code.

In this guide, we will explore the intricacies of scope and closures in JavaScript, understanding their significance and practical applications. We will examine the concept of variable visibility within different scopes, discuss the relationship between lexical scoping and closures, and explore various examples and use cases to solidify our understanding.

2. Scope in JavaScript

Scope is an essential concept in JavaScript that determines the accessibility and visibility of variables throughout your code. Let's explore how it works in JavaScript.

2.1. Definition of Scope

Scope refers to the area of your code where variables and functions are defined and can be accessed. It sets the boundaries for variable visibility and prevents naming conflicts.

2.2. Global Scope

In JavaScript, variables declared outside any function or block have global scope. They can be accessed from anywhere in your code, including within functions and blocks. However, it's important to use global variables judiciously to avoid potential issues with naming conflicts and accidental modifications.

Example 1:


var sportsEvent = 'Olympics';

function globalScopeExample() {
  console.log(sportsEvent); // Accessible within functions
}

globalScopeExample(); // Output: 'Olympics'


Explanation: In this example, we declare a variable sportsEvent with the value 'Olympics' in the global scope. The globalScopeExample() function can access and print the value of sportsEvent because it is defined within the global scope.

2.3. Local Scope

Local scope restricts the visibility of variables to specific functions or blocks where they are defined. It helps encapsulate variables and prevents them from interfering with other parts of your code. JavaScript has two types of local scope: function scope and block scope.

2.3.1. Function Scope

Variables declared within a function are accessible only within that function. They are not visible outside the function or in other functions.

Example 2:


function functionScopeExample() {
  var player = 'Lionel Messi';

  console.log(player); // Accessible within the function
}

functionScopeExample(); // Output: 'Lionel Messi'

console.log(player); // Error: 'player' is not defined


Explanation: In this example, we declare a variable player with the value 'Lionel Messi' within the functionScopeExample() function. The variable player is accessible and can be printed within the function scope. However, if we try to access player outside the function, we'll get an error because it is not defined in the outer scope.

2.3.2. Block Scope (introduced in ES6)

ES6 introduced block scope with the let and const keywords. Variables declared with let and const are scoped to the block in which they are defined (e.g., within if statements or loops), ensuring better control over variable visibility and avoiding issues like variable hoisting.

Example 3:


function blockScopeExample() {
  if (true) {
    let team = 'Real Madrid';

    console.log(team); // Accessible within the block
  }

  console.log(team); // Error: 'team' is not defined outside the block
}

blockScopeExample();


Explanation: In this example, we declare a variable team with the value 'Real Madrid' within the if block. The variable team is scoped to that block and can be accessed and printed within it. However, if we try to access team outside the block, we'll get an error because it is not defined in the outer scope.

These examples demonstrate the concept of scope in JavaScript, highlighting the difference between global scope, function scope, and block scope, and how variables defined within each scope have their own visibility and accessibility rules.

3. Variable Visibility

Variable visibility is a crucial aspect of JavaScript programming, as it determines how and where variables can be accessed within a program. Let's explore three important concepts related to variable visibility: accessing variables within the same scope, understanding variable shadowing, and variable hoisting.

3.1. Accessing variables within the same scope

When variables are defined within the same scope, they can be accessed and used interchangeably. Here's an example to illustrate this in the context of sports:

Example 1:


function calculateScore(goals, assists) {
  const goalPoints = 3;
  const assistPoints = 1;
  const totalScore = (goals * goalPoints) + (assists * assistPoints);
  return totalScore;
}

const playerScore = calculateScore(2, 1);
console.log(playerScore); // Output: 8


In the above code snippet, the goals and assists parameters, as well as the goalPoints, assistPoints, and totalScore variables, are all defined within the calculateScore function's scope. This means that they can be accessed and used within that function without any issues. The totalScore variable, which depends on goals and assists, can be calculated and returned successfully.

3.2. Understanding variable shadowing

Variable shadowing occurs when a variable declared in an inner scope has the same name as a variable in an outer scope. The variable in the inner scope "shadows" or takes precedence over the one in the outer scope. Consider the following example in the context of sports:

Example 2:


const teamName = "Barcelona";

function shadowingExample() {
  const teamName = "Real Madrid";
  console.log(teamName); // Output: "Real Madrid"
}

shadowingExample();
console.log(teamName); // Output: "Barcelona"


In the above code snippet, there are two variables named teamName. The outer teamName is declared globally with a value of "Barcelona". Inside the shadowingExample function, a new teamName variable is declared with a value of "Real Madrid". When we log the value of teamName inside the function, it outputs "Real Madrid", indicating that the inner variable shadows the outer one. Outside the function, the value of the global teamName variable remains unchanged as "Barcelona".

3.3. Variable hoisting and its impact on visibility

Variable hoisting is a JavaScript behavior where variable declarations are moved to the top of their containing scope during the compilation phase. However, only the declarations are hoisted, not the initializations. Take a look at the following example in the context of sports:

Example 3:


function hoistingExample() {
  console.log(playerName); // Output: undefined
  var playerName = "Lionel Messi";
  console.log(playerName); // Output: "Lionel Messi"
}

hoistingExample();


In the above code snippet, even though the playerName variable is logged before it's declared and initialized, it doesn't throw an error. This is because of hoisting. During hoisting, the variable declaration var playerName; is moved to the top of the hoistingExample function, resulting in the first console.log() statement printing undefined. Then, the variable is assigned the value "Lionel Messi", and the second console.log() statement outputs the assigned value.

Note: Gaining a grasp of these fundamental concepts regarding variable visibility in JavaScript is essential for comprehending how variables are accessed and behave within various scopes.

4. Lexical Scoping

4.1. Explaining Lexical Scoping in JavaScript

In JavaScript, lexical scoping refers to the way variables are resolved at the time of their declaration based on the nested structure of functions and blocks. Lexical scoping means that variables are accessible within the scope in which they are defined, as well as any nested scopes. This allows variables to be accessed by functions and blocks defined within their parent scope.

Example:


function outer() {
  var tennisPlayer = 'Roger Federer';

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

  inner();
}

outer();


Explanation: In the code above, the variable tennisPlayer is defined in the outer scope of the outer function. When the inner function is called, it has access to the tennisPlayer variable because of lexical scoping. The output of this code will be 'Roger Federer'.

4.2. Closures and Their Relationship to Lexical Scoping

Closures are closely related to lexical scoping in JavaScript. A closure is created when an inner function retains access to variables in its outer scope, even after the outer function has finished executing. This behavior is possible because of the way JavaScript handles lexical scoping.

Example:


function outer() {
  var nbaPlayer = 'LeBron James';

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

  return inner;
}

var closureFunction = outer();
closureFunction();


Explanation: In this code, the outer function returns the inner function. When outer is called and assigned to closureFunction, the variable nbaPlayer is still accessible to closureFunction due to the closure created. As a result, calling closureFunction() prints 'LeBron James' to the console.

4.3. Benefits of Lexical Scoping and Closures

4.3.1. Data Privacy and Encapsulation

Lexical scoping and closures allow us to create private variables and functions. By enclosing variables within a scope and returning inner functions that have access to those variables, we can control the visibility of data. This helps in achieving data privacy and encapsulation, preventing unwanted modifications or access from outside the scope.

4.3.2. Callback Functions and Event Handlers

Closures are often used in JavaScript for callback functions and event handling. When an event occurs, such as a button click, the associated callback function is executed. Closures enable the callback function to access variables in its surrounding scope, allowing it to work with the necessary data without exposing it globally.

4.3.3. Asynchronous Operations

Closures are essential in handling asynchronous operations in JavaScript. When dealing with timers, AJAX requests, or promises, closures ensure that the necessary data is preserved and accessible when the asynchronous operation completes. This helps maintain the integrity of the program's logic, as the closure carries the relevant state information until it is needed.

Note: Lexical scoping and closures offer powerful capabilities that enable you to write more efficient, modular, and maintainable JavaScript code. These concepts play a crucial role in managing variable visibility, ensuring data privacy, and effectively handling complex scenarios involving asynchronous operations and event-driven programming.

5. Creating Closures

5.1 Defining closures in JavaScript

Closures are an important concept in JavaScript that allow functions to retain access to variables from their outer scope, even after the outer function has finished executing. In simpler terms, a closure is created when an inner function "remembers" its lexical environment, including the variables and functions within its scope.

5.2 Practical examples of closures

5.2.1. Preserving football player statistics

Example:


function createPlayer(name, goals) {
  let playerName = name;
  let playerGoals = goals;

  return {
    getName: function() {
      return playerName;
    },
    getGoals: function() {
      return playerGoals;
    },
    scoreGoal: function() {
      playerGoals++;
      console.log(playerName + " scored a goal! Total goals: " + playerGoals);
    }
  };
}

const player1 = createPlayer("Lionel Messi", 20);
console.log(player1.getName()); // Output: "Lionel Messi"
console.log(player1.getGoals()); // Output: 20
player1.scoreGoal(); // Output: "Lionel Messi scored a goal! Total goals: 21"


In this example, the createPlayer function creates a player object with a name and a goal count. The inner functions getName, getGoals, and scoreGoal have access to the playerName and playerGoals variables through closure. This allows us to encapsulate the player's data and provide controlled access to it.

5.2.2. Tracking football match statistics


function createMatch() {
  let homeTeamScore = 0;
  let awayTeamScore = 0;

  return {
    homeTeamGoal: function() {
      homeTeamScore++;
      console.log("Home Team scored! Total goals: " + homeTeamScore);
    },
    awayTeamGoal: function() {
      awayTeamScore++;
      console.log("Away Team scored! Total goals: " + awayTeamScore);
    },
    getScore: function() {
      return "Home Team: " + homeTeamScore + ", Away Team: " + awayTeamScore;
    }
  };
}

const match1 = createMatch();
match1.homeTeamGoal(); // Output: "Home Team scored! Total goals: 1"
match1.awayTeamGoal(); // Output: "Away Team scored! Total goals: 1"
console.log(match1.getScore()); // Output: "Home Team: 1, Away Team: 1"


In this example, the createMatch function creates a match object with variables to track the scores of the home team and away team. The inner functions homeTeamGoal, awayTeamGoal, and getScore have access to these variables through closure, allowing us to manipulate and retrieve the match statistics.

5.2.3. Managing football team lineups


function createTeam() {
  let lineup = [];

  return {
    addPlayer: function(player) {
      lineup.push(player);
      console.log(player + " added to the lineup.");
    },
    removePlayer: function(player) {
      const index = lineup.indexOf(player);
      if (index !== -1) {
        lineup.splice(index, 1);
        console.log(player + " removed from the lineup.");
      }
    },
    getLineup: function() {
      return lineup;
    }
  };
}

const team1 = createTeam();
team1.addPlayer("Lionel Messi"); // Output: "Lionel Messi added to the lineup."
team1.addPlayer("Cristiano Ronaldo"); // Output: "Cristiano Ronaldo added to the lineup."
console.log(team1.getLineup()); // Output: ["Lionel Messi", "Cristiano Ronaldo"]
team1.removePlayer("Lionel Messi"); // Output: "Lionel Messi removed from the lineup."
console.log(team1.getLineup()); // Output: ["Cristiano Ronaldo"]


In this example, the createTeam function creates a team object with a lineup array. The inner functions addPlayer, removePlayer, and getLineup have access to the lineup variable through closure, enabling us to manage the team's lineup by adding and removing players.

Note: These examples demonstrate how closures can be used in the context of football to preserve player statistics, track match scores, and manage team lineups. Closures offer powerful capabilities for encapsulating data and providing controlled access to it, making them valuable in various aspects of sports-related JavaScript applications.

6. Memory Management in Closures

6.1. Understanding the Closure's Scope Chain and Garbage Collection

In JavaScript, closures have a powerful feature known as the closure scope chain. The scope chain is a mechanism that allows a closure to access variables from its outer scope even after the outer function has finished executing. However, this can also lead to potential memory management issues if not handled properly.

Example:


function outerFunction() {
  var ligue = 'Ligue 1 - France';

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

  return innerFunction;
}

var closure = outerFunction();
closure(); // Outputs: "Ligue 1 - France"


Explanation: In this example, the innerFunction has access to the ligue variable even though it has finished executing. This is because the closure retains a reference to the variables it needs, keeping them alive in memory.

Note: It's important to be cautious with memory management when dealing with closures. If a closure retains unnecessary references, it can cause memory leaks.

6.2. Memory Leaks and Closure Retention

Memory leaks can occur when closures unintentionally retain references to objects that are no longer needed, preventing their garbage collection. This can lead to increased memory usage and potentially degrade the performance of your application.

Let's consider an example to illustrate a memory leak caused by closures:


function createClosure() {
  var players = ['Neymar', 'Mbappe', 'Di Maria']; // Simulating an array of players

  return function() {
    console.log(players);
  }
}

var closure = createClosure();
closure(); // The array of players is still retained in memory


Explanation: In this example, the closure closure retains a reference to the players array, even though it's no longer needed. As a result, the array of players remains in memory, leading to a memory leak.

6.3. Best Practices for Managing Memory in Closures

To manage memory effectively when using closures, consider the following best practices:

6.3.1. Release Unnecessary References

Make sure that closures only retain references to variables that are actually needed. Avoid retaining references to large objects or unnecessary data to prevent memory leaks.


function createClosure() {
  var players = ['Neymar', 'Mbappe', 'Di Maria'];

  return function() {
    console.log('Perform some operations with players');
    players = null; // Release the reference to the array of players
  }
}

var closure = createClosure();
closure(); // The array of players can now be garbage collected


6.3.2. Use IIFE for Temporary Closures

Immediately Invoked Function Expressions (IIFE) can be used when you need a closure for a specific task and don't require long-term retention. IIFE allows you to execute the function immediately and release its memory afterward.


(function() {
  var tempData = 'Temporary data';

  // Perform some operations with tempData
  console.log(tempData);
})(); // IIFE, the closure is immediately released after execution


6.3.3 Be Mindful of Event Handlers

When attaching event handlers, ensure that closures used within event listeners do not inadvertently retain references to DOM elements. Remove event listeners or clear the closures when the elements are no longer needed to prevent memory leaks.


var button = document.getElementById('myButton');

button.addEventListener('click', function() {
  console.log('Button clicked');
  // Remove event listener or nullify the closure if the button is no longer needed
  button.removeEventListener('click', arguments.callee);
});


Note: Remember, managing memory in closures is crucial for long-running applications or scenarios involving large data sets.

7. Advanced Topics

7.1. Immediately Invoked Function Expressions (IIFE)

In JavaScript, Immediately Invoked Function Expressions (IIFE) are powerful constructs that allow you to execute a function immediately after it is defined. They are commonly used to create a private scope and prevent variable pollution in the global scope. Let's explore three examples of IIFE and understand their significance.

Example 1:

(function() {
  // Code executed within the IIFE's private scope
  var championship = "Champions League";
  console.log(championship);
})();


Explanation: In this example, we define an anonymous function and immediately invoke it using parentheses at the end (function() {...})();. The function creates a private scope where we can declare variables, functions, and execute code. In this case, we define a variable championship with the value "Champions League" and log it to the console. The advantage of using an IIFE here is that the championship variable is contained within the function's scope and does not pollute the global scope.

Example 2:

var result = (function(homeTeam, awayTeam) {
  // Code executed within the IIFE's private scope
  return homeTeam + " vs " + awayTeam;
})("Real Madrid", "Barcelona");

console.log(result); // Output: "Real Madrid vs Barcelona"


Explanation: In this example, we define an IIFE that takes two parameters homeTeam and awayTeam. Within the IIFE, we concatenate the two team names and return the result. By immediately invoking the IIFE with the arguments ("Real Madrid", "Barcelona"), the returned value is assigned to the variable result. This approach allows us to encapsulate the concatenation logic within the IIFE and obtain the result in a concise manner.

Example 3:

var teamModule = (function() {
  var team = "";

  return {
    setTeam: function(name) {
      team = name;
    },
    getTeam: function() {
      return team;
    }
  };
})();

teamModule.setTeam("Liverpool");
console.log(teamModule.getTeam()); // Output: "Liverpool"


Explanation: In this example, we create an IIFE that defines a private variable team and returns an object with two methods: setTeam and getTeam. The setTeam method modifies the team variable by assigning a new value, while the getTeam method retrieves its value. By invoking the IIFE and assigning it to the variable teamModule, we effectively create a module with encapsulated state and behavior related to a sports team.

7.2. Module Patterns Using Closures

When you work with JavaScript, module patterns using closures are commonly employed to encapsulate related functionality, promote code organization, and achieve information hiding. Here are three examples of module patterns.

Example 1:

var championsLeagueModule = (function() {
  var champion = "";

  return {
    setChampion: function(team) {
      champion = team;
    },
    getChampion: function() {
      return champion;
    }
  };
})();

championsLeagueModule.setChampion("Bayern Munich");
console.log(championsLeagueModule.getChampion()); // Output: "Bayern Munich"


Explanation: In this example, we create a module using an IIFE. The module defines a private variable champion and returns an object with two methods: setChampion and getChampion. The setChampion method allows us to set the champion team by assigning a value to the champion variable, while the getChampion method retrieves its value. This module pattern allows us to encapsulate the champion team-related functionality and prevent direct access to the champion variable.

Example 2:

var europaLeagueModule = (function() {
  var teams = [];

  return {
    addTeam: function(team) {
      teams.push(team);
    },
    removeTeam: function(index) {
      teams.splice(index, 1);
    },
    getTeams: function() {
      return teams;
    }
  };
})();

europaLeagueModule.addTeam("Arsenal");
europaLeagueModule.addTeam("Sevilla");
console.log(europaLeagueModule.getTeams()); // Output: ["Arsenal", "Sevilla"]

europaLeagueModule.removeTeam(1);
console.log(europaLeagueModule.getTeams()); // Output: ["Arsenal"]


Explanation: In this example, we create a module that manages a list of teams for the Europa League using an IIFE. The module maintains a private array teams and provides methods to add teams, remove teams by index, and retrieve the list of teams. By invoking the module's methods, we can interact with the encapsulated teams array while ensuring that it remains protected and inaccessible from external code.

Example 3:

var playerModule = (function() {
  var players = [];

  return {
    addPlayer: function(player) {
      players.push(player);
    },
    removePlayer: function(index) {
      players.splice(index, 1);
    },
    getPlayers: function() {
      return players;
    }
  };
})();

playerModule.addPlayer("Lionel Messi");
playerModule.addPlayer("Cristiano Ronaldo");
console.log(playerModule.getPlayers()); // Output: ["Lionel Messi", "Cristiano Ronaldo"]

playerModule.removePlayer(1);
console.log(playerModule.getPlayers()); // Output: ["Lionel Messi"]


Explanation: In this example, we create a module that manages a list of players using an IIFE. The module maintains a private array players and provides methods to add players, remove players by index, and retrieve the list of players. By invoking the module's methods, we can manipulate and retrieve the player list while ensuring that the players array remains encapsulated within the module's scope.

7.3. Partial Application and Currying

Partial application and currying are techniques used in functional programming to create new functions by pre-filling some arguments of an existing function. These techniques allow for code reusability and can enhance flexibility in function composition. Let's explore three examples to understand how they work.

Example 1:

function createMatchReport(reporter, league, homeTeam, awayTeam) {
  console.log(reporter + " reporting on " + league + ": " + homeTeam + " vs " + awayTeam);
}

var championsLeagueReporter = createMatchReport.bind(null, "John Doe", "Champions League");
championsLeagueReporter("Real Madrid", "Barcelona"); // Output: "John Doe reporting on Champions League: Real Madrid vs Barcelona"
championsLeagueReporter("Liverpool", "Manchester City"); // Output: "John Doe reporting on Champions League: Liverpool vs Manchester City"


Explanation: In this example, we have a createMatchReport function that takes four arguments: reporter, league, homeTeam, and awayTeam. By using the bind method, we create a new function championsLeagueReporter where the reporter and league parameters are pre-filled with the values "John Doe" and "Champions League", respectively. Now, whenever we call championsLeagueReporter and provide homeTeam and awayTeam arguments, it will invoke the createMatchReport function with the pre-filled values.

Example 2:

function calculateScore(homeTeam, awayTeam, goals) {
  console.log(homeTeam + " " + goals + " - " + awayTeam);
}

function curry(func) {
  return function curried(homeTeam) {
    return function(awayTeam) {
      return function(goals) {
        func(homeTeam, awayTeam, goals);
      };
    };
  };
}

var championsLeagueCurried = curry(calculateScore)("Real Madrid")("Barcelona");
championsLeagueCurried(3); // Output: "Real Madrid 3 - Barcelona"
championsLeagueCurried(2); // Output: "Real Madrid 2 - Barcelona"


Explanation: In this example, we have a calculateScore function that logs the score of a match. The curry function is a utility function that transforms a function into a curried version. It takes a function func and returns a new function that allows partial application. By applying curry to calculateScore, we create a curried version championsLeagueCurried. We then use partial application to pre-fill the homeTeam and awayTeam arguments as "Real Madrid" and "Barcelona", respectively. Finally, we invoke championsLeagueCurried with the goals argument to log the score of the match.

Example 3:

function calculatePrice(basePrice, taxRate, discount, quantity) {
  var totalPrice = basePrice * quantity;
  totalPrice += totalPrice * (taxRate / 100);
  totalPrice -= discount;

  console.log("Total Price: $" + totalPrice.toFixed(2));
}

function partial(func, ...partialArgs) {
  return function(...args) {
    return func(...partialArgs, ...args);
  };
}

var calculatePriceWithTax = partial(calculatePrice, 100, 10, 5);
calculatePriceWithTax(2); // Output: "Total Price: $222.00"
calculatePriceWithTax(5); // Output: "Total Price: $525.00"


Explanation: In this example, we have a calculatePrice function that calculates the total price of a product based on various factors. The partial function is a utility function that allows partial application by pre-filling some arguments. By applying partial to calculatePrice with the arguments (100, 10, 5), we create a new function calculatePriceWithTax where the basePrice, taxRate, and discount arguments are pre-filled. Now, whenever we call calculatePriceWithTax and provide the quantity argument, it will invoke the calculatePrice function with the pre-filled values and calculate the total price.

Note: These advanced topics in JavaScript, including Immediately Invoked Function Expressions (IIFE), module patterns using closures, and partial application/currying, provide powerful techniques to enhance code organization, achieve encapsulation, and promote functional programming principles.

8. Exercises: Scope and Closures in JavaScript

Exercise 1:

Write a function called `createScoreboard` that returns a new function. The returned function should increment a score variable defined in the outer scope each time it is invoked. Test the functionality of `createScoreboard` by creating multiple scoreboards and incrementing their scores individually.

Exercise 2:

Create a function called `secretTeam` that takes a string as an argument. Within the function, define a variable `teamName` and a nested function `revealTeam`. The `revealTeam` function should log the `teamName` variable to the console. Test the behavior of `secretTeam` by invoking the `revealTeam` function from outside its scope.

Exercise 3:

Implement a function called `makeFormula1Race` that takes a number as an argument and returns a new function. The returned function should accept another number as an argument and calculate the total race time by multiplying it with the initial number passed to `makeFormula1Race`. Test the functionality of `makeFormula1Race` by creating multiple race functions with different initial numbers and using them to calculate race times.

Exercise 4:

Create a function called `outerPlayer` that defines a variable `goals` and a nested function `innerPlayer`. Within `innerPlayer`, define another variable `assists` and log the sum of `goals` and `assists` to the console. Invoke `outerPlayer` multiple times with different values for `goals` and observe the output of `innerPlayer`.

8. Solutions: Scope and Closures in JavaScript

Exercise 1:

function createScoreboard() {
  let score = 0;

  return function() {
    score++;
    console.log("Score: " + score);
  };
}

const tennisScoreboard = createScoreboard();
const footballScoreboard = createScoreboard();

tennisScoreboard(); // Score: 1
tennisScoreboard(); // Score: 2
footballScoreboard(); // Score: 1

Explanation: In this exercise, we define a function createScoreboard that returns a new function. The inner function, which is returned, has access to the score variable defined in the outer scope due to closure.

When createScoreboard is called, it creates a separate score variable for each scoreboard. Each time the inner function is invoked, it increments the corresponding score variable and logs the updated score to the console.

We create two scoreboards: tennisScoreboard and footballScoreboard. Invoking tennisScoreboard() twice increments the score by 1 each time. Similarly, invoking footballScoreboard() increments the score for the football scoreboard. As a result, we get the output: Score: 1, Score: 2, Score: 1.

Exercise 2:

function secretTeam(teamName) {
  function revealTeam() {
    console.log("Team Name: " + teamName);
  }

  return revealTeam;
}

const handball = secretTeam("Handball Team");
const formula1 = secretTeam("Formula 1 Team");

handball(); // Team Name: Handball Team
formula1(); // Team Name: Formula 1 Team

Explanation: In this exercise, we define a function secretTeam that takes a teamName parameter. Inside this function, we define a nested function revealTeam that has access to the teamName variable due to closure.

The secretTeam function returns the revealTeam function, which can be invoked from outside its scope. When we invoke handball(), it logs the value of the teamName variable passed during the creation of the handball function, resulting in Team Name: Handball Team. Similarly, invoking formula1() logs Team Name: Formula 1 Team to the console.

Exercise 3:

function makeFormula1Race(initialMultiplier) {
  return function(raceTime) {
    return initialMultiplier * raceTime;
  };
}

const race1 = makeFormula1Race(2);
const race2 = makeFormula1Race(3);

console.log(race1(5)); // 10
console.log(race2(7)); // 21

Explanation: In this exercise, we define a function makeFormula1Race that takes an initialMultiplier parameter. It returns a new function that accepts a raceTime parameter. The inner function calculates the total race time by multiplying the initialMultiplier with the raceTime.

We create two race functions: race1 and race2, with different initial multipliers. When we invoke race1(5), it multiplies the initial multiplier of 2 with the race time of 5, resulting in a total race time of 10. Similarly, invoking race2(7) multiplies the initial multiplier of 3 with the race time of 7, resulting in a total race time of 21.

Exercise 4:

function outerPlayer(goals) {
  return function(assists) {
    console.log("Total Points: " + (goals + assists));
  };
}

const player1 = outerPlayer(10);
const player2 = outerPlayer(5);

player1(3); // Total Points: 13
player2(2); // Total Points: 7

Explanation: In this exercise, we define a function outerPlayer that takes a goals parameter. It returns a new function that accepts an assists parameter. The inner function calculates the total points by adding the goals and assists together.

We create two player functions: player1 and player2, with different goal values. When we invoke player1(3), it adds the goals value of 10 with the assists value of 3, resulting in a total of 13 points. Similarly, invoking player2(2) adds the goals value of 5 with the assists value of 2, resulting in a total of 7 points.

The console output for each invocation displays the total points for the player.

9. Encouragement.

Thank you for investing your time in reading this guide! I trust that you have found it informative and beneficial in enhancing your comprehension of JavaScript Scope and closures.

Ultimately, I strongly urge you to continue your learning journey by delving into the next section [JavaScript DOM manipulation]. Thank you once more, and I look forward to meeting you in the next guide

Comments