JS — Understanding Lexical Environments in JavaScript — Deep Dive — Part 1

nuri

Nuri Ensing

Posted on August 29, 2024

JS — Understanding Lexical Environments in JavaScript — Deep Dive — Part 1

As a developer, I have often encountered the term "lexical environment" but I never really took the time to fully explore it in depth. So, I decided to dive deep and document my findings in this post - because "sharing is caring ;)". By the end of this post, I hope we will both have a solid understanding of what a lexical environment is and we will also explore what happens in memory, what a data structure is, and how the call stack works. Don't worry - I'll keep it simple and clear!

The Lexical Environment

Before diving into the details, let me start with a brief overview. Don't worry if some concepts seem complex at first - I'll break them down and use an analogy to make them easier to understand.

A lexical environment is a special data structure in JavaScript that tracks the scope of variables and functions at a specific point in the code.

Data structures are ways to organize and store information in a computer so it can be used efficiently. Common examples include arrays, objects, lists, and trees. See more: Data Structures Tutorial - GeeksforGeeks

The term "lexical" means that the scope and accessibility of variables and functions are determined by where they are written in the code, rather than how the program runs.

Key roles of a lexical environment:

  • It stores all variables and function declarations within a specific scope (such as a function or a block).
  • It makes these stored items accessible at that particular point in your code.

Here's a simple code example that contains three different lexical environments:

var sheep = 1;  // Global sheep

function drinkWater() {
    let waterTemperature = "cold"; 
    console.log("The sheep is drinking " + waterTemperature + " water.");
}

var sheepHouseInArea = true;  // Indicates whether the sheep house is present

if (sheepHouseInArea) {
    let lambs = 2;  // Lambs inside the house
    console.log("There are " + sheep + " sheep in the total area and " 
                 + lambs + " lambs!");
}

// This will result in an error because 'lambs'
// is only accessible inside the if block!
console.log("How many lambs are there? " + lambs);
Enter fullscreen mode Exit fullscreen mode

The three lexical environments in this code are: the global scope, the drinkWater function scope, and the if block scope. To make these concepts easier to grasp, let's use a simple analogy involving sheep:

The Sheep Analogy:

While walking outside this week, I came across some sheep inside a fenced area and thought, "Hey, this is like a lexical environment!"

Let me explain: Imagine a fenced area with sheep inside. The sheep can only do things within the fence, like eating grass. Now, imagine there's a small sheep house inside the fence where lambs can stay. The lambs inside the house can't go outside, but the sheep outside can go in.

lambs

Breaking Down the Analogy:

The fence represents the entire area where everything exists - the sheep, lambs, house, and grass. This fenced area is what we refer to as the global scope. Within this fenced area, the sheep house is a smaller, separate section, representing a block scope. Finally, the grass which the sheep eat (yumyum) is like a function within the global scope, a specific activity or action that the sheep can perform within that space.

In the code block, the global scope is represented by the red box, the drinkWater function scope by the blue box, and the if block scope by the green box. These are the three lexical environments.

code example picture

Global Scope (Fenced Area):

The sheep (represented by var sheep = 1;) symbolizes a variable in the global scope, freely roaming the fenced area. It can be used both outside and inside the drinkWater function and the if block.

Function Scope (drinkWater):

The drinkWater function represents an action the sheep can perform within the fenced area. We can call the drinkWater function from anywhere in the global scope. However, the function itself creates a new lexical environment when it's defined. Inside this function, variables (like let waterTemperature = 'cold';) are only accessible within the function.

Block Scope (if block):

The if block creates a new, smaller scope. In this scope, represented by the sheep house, there are 2 lambs (let lambs = 2). Inside this scope, a console.log statement logs the value of the lambs variable as well as the global sheep variable. The lambs variable is specific to the block scope, while the sheep variable is fetched from the parent environment (the global scope). This is made possible by the Outer Environment Reference, which allows JavaScript to look up the scope chain and resolve variables not found in the current environment.

The Outer Environment Reference is a reference or a pointer within a lexical environment. It points to the parent lexical environment, allowing JavaScript to resolve variables that aren't found in the current environment by looking up the scope chain.

Question Time!

Can you modify the drinkWater() function so that it logs the total number of sheep defined in the global scope that can drink the water? Share your answer in the comments section!

Understanding Multiple Lexical Environments

So, we see that there are three lexical environments in this code: the global scope, the function scope, and the block scope. When there is more than one lexical environment, we call it multiple lexical environments. It's important to understand that multiple lexical environments can exist in a single piece of code. Each time a new scope is created (e.g., a function or a block), a new lexical environment is generated, meaning different parts of your code can have their own separate environments.

The Environment Record

Now that we understand how lexical environments work, let's dive deeper into the concept of the environment record.

Whenever a lexical environment is created - whether it's for the global scope, a function, or a block - JavaScript automatically generates an environment record for it.

This environment record is a data structure that keeps track of all the variables, functions, and other bindings that are accessible within that specific scope. Essentially, it acts as the internal storage for everything defined within that environment, ensuring that the correct data is available when needed during code execution.

Difference Between Lexical Environment and Environment Record

The key difference between a lexical environment and an environment record:

A lexical environment is the place where JavaScript code runs. Think of it as the "setting" or "context" in which your code exists. This context includes the scope of variables and functions, determining which ones are available or accessible at any point in the code. For example, in our code, the lambs variable is only accessible within the green-bordered environment (the block scope). A lexical environment also includes an Outer Environment Reference (which we already described), allowing access to variables in parent environments.

An environment record is a specific storage area within a lexical environment that holds the actual variables, function declarations, and other identifiers used in that environment. While the lexical environment is the broader context, the environment record is where the code's data - like variable values and function definitions - is stored. Whenever JavaScript needs to access a variable or function, it looks in the environment record of the current lexical environment.

Let's explain the lexical environment and environment record again using our code example:

code example picture

There are three lexical environments, each with its own environment record:

  1. Global Scope (red box), Lexical Environment 1: This lexical environment is created at the global level. The environment record within this environment contains:
    • The variable sheep.
    • The function drinkWater.
    • The variable sheepHouseInArea.

These declarations are accessible throughout the entire code. The global environment also references the function drinkWater, which is defined in this environment, and the if statement, which leads to the creation of its own block scope when executed.

  1. Function Scope (drinkWater, blue box), Lexical Environment 2:
    The environment record in this environment contains the variable waterTemperature, declared using let inside the drinkWater function. This variable is only accessible within the function. However, the function can also access variables in the global environment like sheep.

  2. Block Scope (if block, green box), Lexical Environment 3:
    The environment record within this environment contains the variable lambs, declared using let inside the if block. This variable is only accessible within this specific block scope. The block can also access variables from its parent environment, such as sheep and sheepHouseInArea.

What Happens Behind the Scenes in Memory

After diving deep into lexical environments and environment records, we're now ready to understand how JavaScript manages memory and variable access during code execution.

When your code runs, JavaScript creates a new lexical environment for each function or block of code. Each environment has its own environment record, storing all the variables and functions defined in that scope. This setup ensures efficient memory usage, as we've discussed.

Behind the scenes, the JavaScript engine handles these lexical environments in memory. The call stack is used for tracking function calls, while block scopes create new lexical environments linked to their outer environments. However, unlike functions, these block scopes aren't pushed onto the call stack.

What is the call stack?

The call stack is a fundamental concept in how Javascript executes code.

The call stack is a data structure that keeps track of function calls in a program. It works on a Last-In-First-Out (LIFO) principle. Here's how it works:

  • When a function is called, it's added (pushed) to the top of the stack.
  • When a function finishes executing, it's removed (popped) from the top of the stack.
  • The stack also keeps track of the current position in the code.

Key points about the call stack:

  • It records where in the program we are.
  • If we step into a function, we put it on the top of the stack.
  • If we return from a function, we pop it off the stack.
  • The stack has a maximum size, and if that limit is exceeded, it results in a "stack overflow" error.

slackoverflow

Now you know why its called stack overflow haha!

Here's a simple example to illustrate:

function greet(name) {
    console.log('Hello, ' + name);
}

function processUser(user) {
    greet(user);
}

processUser('Alice');
Enter fullscreen mode Exit fullscreen mode
  1. main() (global execution context)
  2. processUser('Alice')
  3. greet('Alice')

As each function completes, it's popped off the stack until we return to the global context.

The last final question!

In our sheep code example, can you identify if anything is placed on the call stack during execution? Share your thoughts in the comments section!


Conclusion

That's it for Part 1! I hope this post has helped you gain a solid understanding of how JavaScript handles lexical environments, environment records, and what happens behind the scenes in memory. I've learned a lot in the process, and I hope you have too. If you have any questions or feedback, I'd love to hear from you - let's learn and improve together!

I titled this post 'Part 1' because I plan to follow up with 'Part 2,' where I'll dive into three major concepts that are closely linked to lexical environments:

  1. Closures: Think of them as magical boxes that let functions remember and access variables from their outer environment, even after the outer function has finished executing.
  2. Scope Chains: We'll explore how JavaScript navigates through nested environments to find variables, like a treasure hunt in your code.
  3. Hoisting: This explains why some variables and functions seem to "float" to the top of their scope, which can be tricky to understand but is crucial for writing predictable code.

These concepts are super important because they directly impact how your JavaScript code behaves. Understanding them will help you write cleaner, more efficient code and avoid some common pitfalls.

Stay tuned!

--
Please also follow me on my Medium: https://medium.com/@ensing89

💖 💪 🙅 🚩
nuri
Nuri Ensing

Posted on August 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related