How does JavaScript's lexical environment maintain variables declarations within nested block scopes?
Asked Answered
E

2

7

I've read a couple of more comprehensive articles on the execution context and now I am sort of confused and messed up in the head.

To keep the question as brief as possible avoiding long citations I better try to illustrate my mental model through an example focusing on details I can't get, so that you could correct me and point at the mistakes.

Here is an example:

var tomato = 'global tomato';

{
  let tomato = 'block tomato';
  console.log(tomato); // 'block tomato'
}

console.log(tomato); // 'global tomato'

So far, everything is clear. When JS engine created an execution context (global one in our case) var tomato declaration from the first line was placed into a Variable Environment while let tomato within a block scope went into a Lexical Environment. That explains how we ended up with 2 different tomatoes.

Now, let's add another tomato, like here:

var tomato = 'global tomato';

{
  let tomato = 'block tomato';

  {
    console.log(tomato); // ReferenceError: Cannot access 'tomato' before initialization
    let tomato = 'nested block tomato';
  }

  console.log(tomato); // won't reach here
}

console.log(tomato); // won't reach here

ReferenceError is no surprise. Indeed, we tried to access a variable before it's been initialized which is known as Temporal Dead Zone. And that nicely indicates that JS had already created another variable tomato within the most nested block. Also JS had been aware of this tomato being uninitialized at the moment we referenced it. Otherwise, it would have grabbed tomato from the outer scope which is equal to 'block tomato' without throwing any error. So let's fix the error and swap the lines, like this:

var tomato = 'global tomato';

{
  let tomato = 'block tomato';

  {
    let tomato = 'nested block tomato';
    console.log(tomato); // 'nested block tomato'
  }

  console.log(tomato); // 'block tomato' - still 'block tomato'. Nothing has been overwritten.
}

console.log(tomato); // 'global tomato'

What I wonder is how JavaScript manages this most nested block. Because by the time that execution reaches the line:

let tomato = 'nested block tomato';

the Lexical Environment of the execution context already contains the variable tomato which was initialized in the outer scope with the value of 'block tomato'. Assuming JS doesn't create a new execution context (with Lexical and Variable environments respectively) just for the blocks of code (that's only the case for function invocations and global script, right?) and obviously, it doesn't override variables in existing Lexical Environment with the ones having the same name but coming from the nested block scopes. As the last piece of code shows, where a brand new, independent variable was created to hold a value 'nested block tomato'.

Then the question is where exactly is this variable stored? I mean there is only one Lexical Environment for an execution context but we might create a numerous nested scopes declaring variables inside. I'm struggling to visualize where these variables would be stored and how this whole thing fit together.

Elwaine answered 24/10, 2020 at 10:58 Comment(3)
the explaination is, javascript looks for the variable in its environment first, then it loads it would look higher up the scope and higher up until global, and if none exists in global, then it would give an undefined error or something like thatPrincipium
The fact that these variables are all called tomato is irrelevant. They could be called x, y, and z, and you'd get the same behaviour, it's only easier to see for a person that they aren't related. Each time you lookup a variable, the lookup starts from the current environment and if not found there, goes through the chained environments up. There is no clash or confusion between two environments, as they are different variables.Gabbie
Chrome dev tools debugger can show you scopes aka lexical environment.Almoner
W
5

Assuming JS doesn't create a new execution context (with Lexical and Variable environments respectively) just for the blocks of code (that's only the case for function invocations and global script, right?)

That's an incorrect assumption.

See the specification:

A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment. Usually a Lexical Environment is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement and a new Lexical Environment is created each time such code is evaluated.

The block statement creates a new lexical environment.

Windswept answered 24/10, 2020 at 11:9 Comment(0)
D
4

In ECMAScript, lexical scopes can be nested.

I mean there is only one Lexical Environment for an execution context but we might create a numerous nested scopes declaring variables inside.

No, there is one Lexical Environment for … well … each Lexical Environment. ("Scope" is just a different term for "Environment" after all.)

The specification does not force the implementor to implement this in any specific way. A typical way could be to have linked list of environments.

Dutiful answered 24/10, 2020 at 11:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.