Diving Deep: Unraveling Scope and Closures in JavaScript

Anton Ioffe - August 17th 2023 - 3 minutes read

Introduction

In the vast world of JavaScript, two concepts reign supreme in terms of complexity and importance: scope and closures. Both integral to the structure and execution of code, they shape the way variables and functions interact. As we explore these concepts, keep in mind that mastering them can transform your JavaScript journey, making you not just a coder, but a craftsman.

Scope in JavaScript

What is scope in JavaScript? In essence, scope defines the accessibility and life cycle of variables and parameters in some particular region of your code during runtime. Think of it as the universe in which a variable exists. There are three primary types of scope: global, local (or function), and block scope.

Global Scope: Variables declared outside a function, accessible from any other code in the document.

var globalVar = "I'm a global variable!";

Local (Function) Scope: Variables declared within a function, accessible only within that function.

function greet() {
  var localVar = "Hello!";
  console.log(localVar); // Outputs: Hello!
}
// localVar is not accessible here

Block Scope: This is where let and const come into play. They confine variables to the block, statement, or expression where they are used, essentially within the nearest set of curly braces.

if (true) {
  let blockScopedVar = "I'm block scoped!";
  const blockScopedConst = "Me too!";
}
// blockScopedVar and blockScopedConst are not accessible here

Closures in JavaScript

What are closures? A closure is a function bundled with its lexical environment. This means the function carries references to variables from outside its current scope, providing a way to retain state between function calls.

For example:

function outerFunction() {
  let counter = 0;

  return function innerFunction() {
    counter++;
    return counter;
  };
}

const incrementer = outerFunction();

console.log(incrementer()); // 1
console.log(incrementer()); // 2

This behavior is a direct product of two things: how JavaScript handles scope and how functions are first-class citizens, capable of being returned by other functions.

Deep Dive into Lexical Scope

Lexical Scope, also known as static scope, refers to the fact that every inner level can access its outer levels. The term "lexical" is used because lexical scope is scope based on where variables and blocks of scope are authored, by you, at write time.

Consider:

function grandfather() {
  const name = 'Abraham';

  return function father() {
    const name = 'John';

    return function son() {
      const name = 'William';

      return `${name} is the son of ${father.name}, who is the son of ${grandfather.name}`;
    };
  };
}

// Outputs: William is the son of John, who is the son of Abraham
console.log(grandfather()()()); 

Here, the son function has access to variables in both the father and grandfather scopes, thanks to lexical scope.

Scope Chain and Its Relation to Closure

In the world of JavaScript, the difference between scope chain and closure is nuanced. The scope chain is a series of variable objects created at every scope level, ensuring each function has access to variables in its containing scopes.

Closures utilize the scope chain. When a function is defined, it retains access to its lexical scope, and that, in essence, forms a closure.

Challenging Concepts and Tricky Questions

Many find closures to be an enigma. And indeed, their behavior in JavaScript does require a deeper understanding:

How do closures differ in JavaScript vs. other languages? In JavaScript, closures are often seen in callbacks, promises, and event handlers. They help retain state and context over asynchronous operations, a common pattern in the event-driven world of JavaScript.

Challenging Task for Junior/Middle Developer

Task: Create a function createSpecialLogger() that returns an object with methods to log different kinds of messages. This object should have three methods:

  • info(message) to log informational messages.
  • warn(message) to log warning messages.
  • error(message) to log error messages.

However, there's a twist. For every logged message, the logger should prepend the kind of message (i.e., INFO, WARN, or ERROR) before the actual message. Additionally, for every 10th message, regardless of its type, the logger should log "You've logged 10 messages!".

Hint: Think about how you might leverage closures to keep track of the total number of logged messages without exposing this number.

Sample Usage:

const logger = createSpecialLogger();

// Outputs: INFO: This is an info message.
logger.info('This is an info message.');  

// Outputs: WARN: Be cautious!
logger.warn('Be cautious!');

// ... after 8 more logs ...
// Outputs: ERROR: Oops, something broke.
// Outputs: You've logged 10 messages!
logger.error('Oops, something broke.');   
Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers