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.
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.