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.
tomato
is irrelevant. They could be calledx
,y
, andz
, 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