Deep Dive into JavaScript: Lexical Scope, Closures and “this” keyword

V
5 min readMar 25, 2024

--

Introduction

JavaScript, widely known as the programming language of the Web, proves to be a fundamental tool for creating interactive user interfaces and powering backend functionalities. As we delve deeper into the world of JavaScript, we come across intriguing concepts like lexical scope, closures, and the frequently misunderstood this keyword. In this article, I aim to shed light on these three concepts and demonstrate how lexical scoping intertwines with closures and the dynamic resolution of the this keyword.

Understanding Lexical Scope

When we talk about lexical scope in JavaScript, we’re referring to how variables and functions become accessible based on their placement within the code’s physical structure, such as nested blocks or functions. This concept is fundamental, since JavaScript uses lexical (static) scoping to resolve variable names at write time.

Lexical scope:

  • Cares about where variables and functions are defined.
  • Considers nesting of blocks or functions.
  • Determines scope based on where variables and functions are defined within this nested structure, not just where the code physically exists in the file.

This leads to the following lexical scoping rules:

  • Inner functions or blocks can access variables declared in their outer function or block.
  • Inner scopes can reassign variables declared with let or var from outer scopes within the same scope or inner scopes.
  • Inner scopes cannot directly modify const variables declared in outer scopes by reassigning the entire variable reference. However, they can modify properties of objects and arrays declared with const if those properties are mutable.
  • When a variable is referenced, JavaScript looks for variables or functions starting from the current scope and then progressively through outer scopes until reaching the global scope, allowing for a hierarchical resolution of identifiers.

Let’s consider the following example:

function greet() {
const message = 'Hello';

function sayHello(name) {
console.log(`${message}, ${name}!`);
}

sayHello('John'); // output: Hello John!
}

greet();

In this example, message is defined within the greet function's scope, allowing the sayHello function to access it due to lexical scope rules. When greet is called, it invokes sayHello with the argument 'John', resulting in the output "Hello, John!".

Now, how about the next one?

function greet() {
let greeting = 'Hello';

const person = { name: 'Alice' };

function updateGreeting(newGreeting) {
greeting = newGreeting;
console.log(greeting); // Output: Updated greeting
}

function updateName(newName) {
person.name = newName; // Modifying a property of an object declared with const
console.log(person.name); // Output: Bob
}

function updatePerson(newName) {
// Attempting to reassign the entire object reference for a const variable (will result in an error)
person = { name: newName }; // Error: Assignment to constant variable
}

updateGreeting('Updated greeting');
updateName('Bob');
updatePerson('Jane');
}

greet();

Let’s break down the behavior of the three inner functions:

  • updateGreeting: This function takes a new greeting as an argument and updates the greeting variable declared with let. Since let variables are mutable, we can reassign their values within the same scope or inner scopes. Therefore, we get the output as the updated value "Updated greeting".
  • updateName: This function takes a new name as an argument and modifies the name property of the person object declared with const. The const keyword allows the modification of properties if the properties themselves are mutable. Hence, this function successfully updates and logs the new name "Bob".
  • updatePerson: This function attempts to reassign the entire object reference of the person object to a new object with a different name. However, since person is declared with const, it cannot have its reference reassigned after initialization. This attempt to reassign the object reference leads to an error (TypeError: Assignment to constant variable).

Now that we have a solid understanding of lexical scoping rules, let’s delve into one of the most powerful concepts that stems from it: closures.

Exploring Closures

Closures in JavaScript are like a dynamic duo formed by a function and the environment it’s created in. They work hand in hand with lexical scoping, allowing a function to retain accessibility to its surrounding variables even after the outer function has finished executing.

Take a look at the following snippet:

function outerFunction() {
const message = 'Hello';

function innerFunction() {
console.log(message); // Accessing message from outerFunction
}

return innerFunction;
}

const myClosure = outerFunction();
myClosure(); // Output: Hello

In this example, innerFunction forms a closure with the message variable defined in outerFunction. Even after outerFunction completes its execution, myClosure retains access to message due to the closure mechanism, leading to the output "Hello".

It’s important to note that closure formation occurs during the definition of the inner function, and not during runtime.

Now that we’ve explored how closures work within the context of lexical scope, let’s shift our focus to another crucial aspect: the resolution of the this keyword.

Decoding this Keyword

In JavaScript, the this keyword is a special identifier that refers to the current execution context within which a function is invoked. It plays a crucial role in determining the context for accessing properties and methods.

The resolution of this varies depending on the type of function being used, particularly between regular functions and arrow functions.

Consider the following example:

const obj = {
name: 'Alice',
greet: function() {
return function() {
return this.name;
};
},
arrowGreet: function() {
return () => this.name;
}
};

const regularGreet = obj.greet(); // Returns a regular function
const arrowGreet = obj.arrowGreet(); // Returns an arrow function

console.log(regularGreet()); // Output: undefined
console.log(arrowGreet()); // Output: Alice

The greet method returns a regular function that tries to access this.name, but because it's a regular function, this inside it refers to the global scope (or undefined in strict mode), resulting in the output being undefined.

On the other hand, the arrowGreet method returns an arrow function. Arrow functions do not have their own this context; instead, they inherit this from the surrounding lexical scope, which in this case is the obj object. Therefore, this.name inside the arrow function correctly refers to the name property of the obj object, resulting in the output being "Alice".

Hence, we can observe that:

  • Arrow functions maintain the lexical scope of this.
  • Regular functions have their own this context that depends on how they are invoked.

Conclusion:

In this journey through JavaScript’s foundational concepts, we’ve explored the workings of lexical scope, closures, and the dynamic resolution of the this keyword.

Lexical scope, with its emphasis on variable and function accessibility based on code structure, provides a structured approach to managing scope and defining function behavior.

Closures, as dynamic pairs between functions and their environments, enable functions to retain access to variables even after their outer function execution completes, adding a layer of flexibility and persistence to our code.

The resolution of the this keyword clarifies the context within which functions operate.

By mastering these concepts, JavaScript developers can gain a deeper understanding of the language’s inner workings, enabling them to write more efficient and robust code. I hope that this journey through JavaScript’s foundational concepts has provided valuable insights for developers. Happy coding!

--

--