Frontend
Basic
Josué Hernández

Josué Hernández

    What is Scope?
    Global Scope
    Local Scope
    Function Scope
    Block Scope
    What is Hoisting?
    Examples of Hoisting
    Practical Tip: To avoid confusion and potential bugs:
    What is a Closure?
    Basic Closure Example
    Practical Example: A Counter
    Customizable Counters
    Mastering Closures: Unlocking Their Power
    Maintaining State
    Creating Private Variables
    Functional Patterns
    Why Closures Matter
    Key Reasons Closures Matter
    Real-World Applications

In modern JavaScript, understanding scope and closures is essential for writing efficient and maintainable code. These concepts govern how variables are accessed and retained, enabling powerful patterns and practices.


What is Scope?

Scope defines the visibility and lifetime of variables within your code. It determines which variables you can access and where. JavaScript has several types of scope, including global, local, function, and block scope, all of which serve specific purposes.


Global Scope

Variables in the global scope are declared outside any function or block. They are accessible from anywhere in your code. However, using global variables is discouraged because they can lead to conflicts or unexpected behavior.

Example:

JAVASCRIPT
const isStudent = true;

function checkStudent() {
  console.log(isStudent); // true
}

checkStudent();
console.log(isStudent); // true

In this example, isStudent is a global variable, accessible both inside the checkStudent function and outside of it. However, global variables can lead to unexpected results if multiple parts of your code inadvertently modify them.


Local Scope

Local scope applies to variables declared inside a function or block. They are only accessible within that specific function or block.

Example:

JAVASCRIPT
function myFunction() {
  const carName = "Volvo";
  console.log(carName); // Volvo
}
myFunction();
// console.log(carName); // Error: carName is not defined

The variable carName exists only inside myFunction. Trying to access it outside the function results in an error because its scope is limited to the function.


Function Scope

Variables declared with var have function scope, meaning they are accessible throughout the entire function, even before their declaration (due to hoisting). Variables declared with let or const are block-scoped.

Example:

JAVASCRIPT
function example() {
  if (true) {
    var a = 10;
    let b = 20;
    const c = 30;
  }
  console.log(a); // 10
  // console.log(b); // Error: b is not defined
  // console.log(c); // Error: c is not defined
}
example();

Here, a is declared with var, so it is accessible throughout the function, including outside the if block. In contrast, b and c, declared with let and const, are confined to the if block.


Block Scope

let and const introduced block scope in ECMAScript 6. This limits the scope of a variable to the block (delimited by {}) in which it was declared.

Example:

JAVASCRIPT
{
  let blockScoped = "I'm inside a block!";
  console.log(blockScoped); // I'm inside a block!
}
// console.log(blockScoped); // Error: blockScoped is not defined

The variable blockScoped only exists within the {} block. Attempting to access it outside the block results in an error.


What is Hoisting?

Hoisting is JavaScript's default behavior of moving declarations to the top of their containing scope during compilation. This means variables and functions are registered in memory before the code is executed, allowing you to reference them before they are declared.

Key Points:

  • Only declarations are hoisted; initializations are not.
  • var declarations are hoisted and initialized with undefined.
  • let and const declarations are hoisted but remain uninitialized (in the "temporal dead zone").
  • Functions are hoisted with their entire body.

Examples of Hoisting

Example 1: var and Hoisting

JAVASCRIPT
console.log(a); // undefined
var a = 10;
console.log(a); // 10

Here, the declaration of a is hoisted to the top of the scope, but the assignment a = 10 remains in place. Therefore, console.log(a) before initialization returns undefined.


Example 2: let and const

JAVASCRIPT
console.log(b); // Error: Cannot access 'b' before initialization
let b = 20;

console.log(c); // Error: Cannot access 'c' before initialization
const c = 30;

Although b and c are hoisted, they remain in the temporal dead zone until the execution reaches their declaration. Accessing them before this point results in a ReferenceError.


Example 3: Function Hoisting

JAVASCRIPT
sayHello(); // Hello, world!

function sayHello() {
  console.log("Hello, world!");
}

Functions are hoisted with their complete definition, so they can be called before their declaration.

Practical Tip: To avoid confusion and potential bugs:

  • Use let or const for variable declarations to limit hoisting-related issues.
  • Declare variables at the top of their scope for clarity.
  • Avoid relying on hoisting to manage function execution order.

What is a Closure?

A closure is the combination of a function and the scope in which it was created. Closures allow functions to "remember" variables from their enclosing scope even after that scope has finished executing.


Basic Closure Example

JAVASCRIPT
function outerFunction() {
  const outerVariable = "I'm from the outer scope!";

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

  return innerFunction;
}

const closureExample = outerFunction();
closureExample(); // Output: I'm from the outer scope!

The innerFunction retains access to the outerVariable from the outerFunction, even though outerFunction has completed execution. This is the essence of a closure.


Practical Example: A Counter

JAVASCRIPT
function createCounter() {
  let count = 0;

  return function () {
    count++;
    console.log(count);
  };
}

const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

The count variable is preserved between function calls because of the closure created by the returned function. This allows you to maintain and manipulate state.


Customizable Counters

JAVASCRIPT
function buildCounter(start = 0) {
  let count = start;

  return {
    increment: () => ++count,
    decrement: () => --count,
    reset: () => (count = start),
  };
}

const counter = buildCounter(10);
console.log(counter.increment()); // 11
console.log(counter.decrement()); // 10
counter.reset();
console.log(counter.increment()); // 11

Here, the count variable is encapsulated within the buildCounter function, accessible only through the methods increment, decrement, and reset.


Mastering Closures: Unlocking Their Power

Closures are one of the most versatile tools in JavaScript, enabling developers to write expressive and efficient code. They form the backbone of many advanced patterns and functionalities in modern programming. Here's how closures can empower your code:

Maintaining State

Closures allow you to retain and manage state across function calls without relying on global variables. This makes your code more predictable and encapsulated.

Example:

JAVASCRIPT
function userTracker() {
  let loggedIn = false;

  return {
    logIn: () => {
      loggedIn = true;
      console.log("User logged in:", loggedIn);
    },
    logOut: () => {
      loggedIn = false;
      console.log("User logged out:", loggedIn);
    },
    status: () => {
      console.log("User is logged in:", loggedIn);
    }
  };
}

const user = userTracker();
user.status(); // User is logged in: false
user.logIn();  // User logged in: true
user.status(); // User is logged in: true
user.logOut(); // User logged out: false

Here, the loggedIn variable is private to the userTracker function, but its state is maintained across calls through the closure.

Creating Private Variables

In JavaScript, closures are often used to emulate private variables, as the language lacks built-in privacy controls.

Example:

JAVASCRIPT
function bankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit: (amount) => {
      balance += amount;
      console.log(`Deposited $${amount}. New balance: $${balance}`);
    },
    withdraw: (amount) => {
      if (amount > balance) {
        console.log("Insufficient funds!");
      } else {
        balance -= amount;
        console.log(`Withdrew $${amount}. New balance: $${balance}`);
      }
    },
    getBalance: () => {
      console.log(`Current balance: $${balance}`);
    }
  };
}

const myAccount = bankAccount(100);
myAccount.deposit(50);    // Deposited $50. New balance: $150
myAccount.withdraw(30);   // Withdrew $30. New balance: $120
myAccount.getBalance();   // Current balance: $120

The balance variable is accessible only through the returned methods, ensuring it cannot be directly modified from outside the bankAccount function.

Functional Patterns

Closures are foundational for functional programming patterns like currying and partial application.

Example:

JAVASCRIPT
function multiplyBy(multiplier) {
  return function (value) {
    return value * multiplier;
  };
}

const double = multiplyBy(2);
const triple = multiplyBy(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

The multiplyBy function generates specialized functions (double and triple) that retain their own multiplier values via closures, showcasing their utility in building reusable functional components.


Why Closures Matter

Closures are fundamental to JavaScript's flexibility and power. Their ability to encapsulate data, manage state, and create reusable components makes them invaluable in modern web development.

Key Reasons Closures Matter

  • Encapsulation and Privacy: They enable you to create private variables and restrict access to internal logic.
  • State Management: Closures allow functions to retain memory of their enclosing scope, reducing the need for global variables.
  • Code Modularity: By breaking functionality into smaller, self-contained units, closures promote clean and maintainable code.
  • Functional Programming: Closures enable patterns like currying, partial application, and higher-order functions.

Real-World Applications

  • Event Listeners: Retain references to specific data when events are triggered.
  • React Hooks: Custom hooks in React heavily rely on closures to manage component state.
  • Debouncing and Throttling: Closures help retain state and timing information for these performance optimization techniques.

Closures are more than a concept; they are a practical tool that empowers developers to write scalable, efficient, and elegant code. Understanding and mastering closures will significantly elevate your JavaScript expertise.



Josué Hernández
Josué Hernández

Last Update on 2025-01-07

Related Blogs

© 2024 Effort Stack. All rights reserved