What is the difference and relationship between execution context and lexical environment?
The best way to think of an execution context is as a stack frame, while lexical environments are indeed the scopes.
The respective spec chapters (§8.1 Lexical Environments and §8.3 Execution Contexts) explain:
- Execution contexts contain the current evaluation state of code, a reference to the code (function) itself, and possibly references to the current lexical environments.
Execution contexts are managed in a stack. - Lexical environments contain an environment record in which the variables are stored, and a reference to their parent environment (if any).
Lexical environments build a tree structure.
With every change of the execution context, the lexical environment changes as well. However the lexical environment may change independently from that as well, for example when entering a block.
Execution Context & Execution Context stack : Execution context is the internal javascript construct to track execution of a function or the global code. The js engine maintains a stack - execution context stack or call stack, which contains these contexts and the global execution context stays at the bottom of this stack. And a new execution context is created and pushed to the stack when execution of a function begins. A particular execution context tracks the pointer where statement of the corresponding function is being executed. An execution context is popped from the stack when corresponding function's execution is finished.
Lexical Environment : it's the internal js engine construct that holds identifier-variable mapping. (here identifier refers to the name of variables/functions, and variable is the reference to actual object [including function type object] or primitive value). A lexical environment also holds a reference to a parent lexical environment.
Now, for every execution context -- 1) a corresponding lexical environment is created and 2) if any function is created in that execution context, reference to that lexical environment is stored at the internal property ( [[Environment]] ) of that function. So, every function tracks the lexical environment related to the execution context it was created in.
And every lexical environment tracks its parent lexical environment (that of parent execution context). As a result, every function has a chain of lexical environments attached to it. [Note: in js a function is an object, creating a function by a statement means creating an object of type Function. So like other objects, a function can hold properties both internal and user defined]
The js engine search any variable or function identifier in the current lexical environment, if not found it searches the chain attached to the enclosing function. (for global code this chain does not exist). So, you understand how scoping of variables and functions are maintained. The concept of closure is also backed by, this chain (not a standard term, I just used it to ease the understanding). When a function instance is passed to another function as an argument, to be used as callback, it carries it's chain with it (sort of).
Note: the answer is based on what I learned from 'Secrets of the Javascript Ninja, 2/e'
The answer marked above likened the Execution Context to a Stack Frame. But the Execution Context in JavaScript is no ordinary Stack Frame.
In the global Execution Context, JavaScript Engine creates two things for you, a Global Object (an Object is a collection of name/value pairs) and a special variable called 'this'. In browsers, the Global Object is a window object. In NodeJS, the global object is something else. The point is there is always a global object.
When you create variables and functions that are not inside other functions, those variables are in the global context and thus get attached to the global object, which in the case of browsers is the window object.
hello = 'hello world'
> "hello world"
window.hello
> "hello world"
The execution context in JavaScript is created in two phases. The first phase is the Creation Phase. In the global execution context, the Global Object is setup and in memory, the special variable 'this' is setup, points to Global Object and is in memory, and there is an Outer Environment (Lexical Environment). As the Parser begins the Creation Phase of the execution context, it first recognizes where you created variables and functions. So the Parser sets up memory space for variables and functions. This step is called 'Hoisting'. Hence, before your specific code is executed line by line, the JavaScript Engine already set aside memory space for the variables and functions you created in the global Execution Context:
console.log(a);
console.log(b());
console.log(d);
var a = 'a';
function b(){
return 'called from b';
}
function c(){
d = 'd';
}
> undefined
> called from b
> Uncaught ReferenceError: d is not defined
In the above example, since the variable 'a' and the function b() were created in the global Execution Context, memory space is allocated for them. Note though that the variables are not initialized, just declared with an undefined value. This is not the case for functions. Functions are both declared and initialized, so both the identifier and the actual code of the function is stored in memory. Also note that since d (even though it is not specified with var, let or const) is not in the global Execution Context, no memory space is allocated for it. And so an exception is raised when we try to access the d identifier.
Now if we invoke c() before we reference the d variable, then a new execution context is evaluated (which is not the global Execution Context) and then d will be in memory (in that new execution context, the this keyword will point to the global object since we did not place 'new' before the function invocation and so d will be attached to the global object):
console.log(a);
console.log(b);
console.log(c());
console.log(d);
var a = 'a';
function b(){
return 'called from b';
}
function c(){
d = 'd';
return 'called from c';
}
> undefined
> b() { return 'called from b' }
> called from c
> d
One final point about the Creation Phase of the Execution Context. Since 'hoisting' occurs, order of function definitions or variables do not matter in terms of lexical scoping.
The second phase of the Execution Context is called the Execution Phase, and this is where assignments occur. The JavaScript engine begins parsing your code. So that is when variables will be assigned a value. In the first phase, they were just declared and stored in memory with an undefined value. 'undefined' is a placeholder which is JavaScript's way of saying 'I don't know what this value is yet'. This is the same placeholder JavaScript gives when you declare a variable without assigning it a value. Consequently, it is not a good idea to rely on JavaScript's 'Hoisting' feature. Simply put, do not use a variable in global Execution Context (or in any execution context) before it is declared with var, const or let. So it is always better to do this:
var a = 'a';
function b(){
return 'called from b';
}
console.log(a);
console.log(b());
Do not confuse yourself with the JavaScript built-in data type 'undefined' vs an undefined exception raised by the parser. When a variable is not declared anywhere and you try to use it, the JavaScript Engine will raise an exception 'Uncaught ReferenceError: [variable] is not defined'. JavaScript is saying that the variable is not in memory. This is different then initializing a variable with the undefined data type.
In addition to the global Execution Context, function invocation creates a new Execution Context. First, in the below example, a global Execution Context is created. The Creation Phase of the global Execution Context will be handled by the JavaScript Engine. It will create a Global Object (window in the case of browsers), and it will create a special variable 'this' and point it to the Global Object. Then the b and a function will be attached to the Global Object. Memory space will be allocated for them, and since they are functions, their code will be stored in memory as well. If there were any variables, they will be stored in memory too, but they will not be initialized and thus stored with the data type undefined. Then the Execution Phase begins. Since JavaScript is single-threaded, it executes line by line. During this time, it encounters a(). It sees the '()' and knows it must invoke the function a. A new Execution Context is created and placed on the Execution Stack. As you probably know, the stack data structure is last in first out. An Execution Context is pushed onto the Execution Stack, and when it is finished, it is popped from the stack. Whatever context is on top, that is the Execution Context that is currently running.
function b(){
}
function a(){
b();
}
a();
The a() Execution Context will be stacked on top of the global Execution Context. It will have its own memory space for local variables and functions. It will go through the Creation Phase and Execution Phase of the Execution Context. During a() Execution Context's Creation Phase, since it did not declare any variables or functions, it does not allocate space for any new variables or functions. If it did declare any local variables or functions, it will go through the same 'Hoisting' process as is the case in the global Execution Context. Also, a new special 'this' variable is created for that particular function. Note though if you invoke the function without the new keyword, then 'this' will still reference to the global object, which is the window object in browsers.
Then, it moves on to the Execution Phase. Here it invokes the b() function, and now a third Execution Context is created. This is the b() Execution Context. It goes through the same Creation and Execution Phase as the other Execution Contexts'.
When b() finishes, it is popped off the stack, and then when a() finishes, it will be popped off the stack, and then we return back to the global Execution Stack. Importantly, each execution context stores a pointer to where it left off when it invoked a function and hence created a new execution context. So when b() finishes, a() returns to the statement wherein it invoked b(). And then continues execution on the next statement in that execution context. Again, remember, JavaScript is single-threaded, so it executes line by line.
The key about the Lexical Environment is it has a link to any Outer Environment (i.e. its scope chain), so it is used to resolve identifiers outside the current Execution Context. Ultimately, a corresponding Lexical Environment is created for every Execution Context. The Lexical Environment cares about where the code sits physically (lexically) in your application.