In this article, I’ll delve into one of the most basic concepts in JavaScript, which is the execution context. Through this article, you should have a clear idea of what JS interpreters do, why they can be used before some functions and variables are declared, and how their values are determined.
What is an Execution Context?
When JavaScript code is running, the execution environment it is in is very important, and is generally considered to be one of the following:
- Global Code – The default environment, where your code is first executed.
- Function code – when code execution enters the Function body.
- Eval Code – The text that is executed inside the Eval function.
There are many articles about scopes available online, and the purpose of this article is to make it easier for you to understand these concepts. Let’s imagine that the term execution context is the execution environment/scope of the current code. Without further explanation, let’s look at an example of code that executes in both a global and a function/local context.
Nothing special here, we have one global context (with a purple border) and three different function contexts (with a green, blue, and orange border). There can be only one global context and it can be accessed by other contexts in the program.
You can have multiple function contexts, and each function call creates a new context and creates a local scope. Anything declared inside the scope cannot be accessed outside the scope of the current function. In the above example, the function can access declared variables outside the current context, but not vice versa. Why is that? How does this code actually work?
Execution Context Stack
The JavaScript interpreter in the browser is single-threaded. What this really means is that only one thing happens at a time in the browser, and other actions or events queue up in the so-called execution stack. The following icon is an abstract representation of a single-threaded stack:
We already know that the first time a browser loads a script, it defaults into the global execution context. If, in the global context, you call a function, your sequence flows into the function being called, creating a new execution context and pushing that context onto the execution stack.
The same thing happens if you call another function within the current function. The execution flow of the code goes into the inner function, which creates a new execution context that is pushed onto the top of the existing stack. The browser always executes the execution context at the top of the current stack. Once the function completes execution in the current execution context, it pops out of the top of the stack and passes control to the next context in the current stack. The following code shows a recursive function and the execution context of the program:
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));
Copy the code
This code calls itself three times, increasing the value of I by one each time. Each time the function foo is called, a new execution context is created. Once the context completes, it pops off the stack and returns control to the next context until the global context is accessed again.
There are five key points to keep in mind about execution context:
-
Single thread.
-
Synchronous execution.
-
There is only one global context.
-
There can be an infinite number of function contexts.
- Each function call creates a new one
Execution context
Even if it’s a recursive call.
Details in the execution context
We now know that each function call creates a new execution context. However, inside the JavaScript interpreter, calls to each execution context go through two phases:
- Creation [when a function is called but the internal code has not yet started executing]:
- Create scope chains.
-
Create variables, functions, and parameters
- Determines the value of “this”
- Activation/code execution phase:
-
Assign values, find function references, and interpret/execute code
We can represent the execution context with a conceptual object with three properties:
ExecutionContextObj = {'scopeChain': {/* Variable objects + all variable objects in the parent execution context */}, 'variableObject': {/* Function arguments/arguments, internal variables and function declarations */}, 'this': {}}Copy the code
Active object/Variable object [AO/VO]
This executionContextObj object is created when the function is called, but before the function is actually executed. This is what we call the phase 1 build phase. At this stage, the interpreter creates the executionContextObj object by scanning the parameters of the passed function, local function declarations, and local variable declarations. The result of this scan becomes a variableObject in executionContextObj.
Here’s a pseudo-overview of how the interpreter executes code:
-
Look for the code that calls the function
- In the implementation
function
Before creating the codeExecution context
. -
Enter the creation phase:
- Initialize the scope chain.
- create
The variable object
: - create
Parameter object
, checks the context of the parameter, initializes its name and value, and creates a reference copy. -
Function declarations in scan context:
- For each function that is discovered, in
The variable object
Create an attribute with the same name as the function’s name. this is an in-memory reference to the function. -
If the function name already exists, the reference value will be overwritten.
-
Variable declarations in scan context:
- For each discovered variable declaration, in
The variable object
Create a property with the same name and initialize it with a value ofundefined. - If the variable name is in
The variable object
Is already there. Do nothing. Keep scanning. - Determine the context of “this”
-
Activation/code execution phase:
-
Executes/interprets function code in context and assigns values to variables as the code executes line by line.
Let’s look at an example:
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
Copy the code
When foo(22) is called, the create phase looks like this:
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, this: { ... }}Copy the code
As you can see, the create phase is responsible for defining property names, not assigning them values, except for parameters. Once the creation phase is complete, the execution flow enters the function. After function execution, the activation/code execution phase looks like this:
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: 'hello', b: pointer to function privateB() }, this: { ... }}Copy the code
About ascending (collieries)
You can find a lot of information on the web about the term promotion in JS, which explains the mechanism by which variable and function declarations are “promoted” to the top of their function function. However, none of this explains in detail why this happens, and it’s easy to understand if you’re just learning something new about how the interpreter creates live objects. Here’s another example:
(function() {
console.log(typeof foo); // function pointer
console.log(typeof bar); // undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
}());
Copy the code
These are the questions we can now answer:
- Why should we be able to access foo before it’s declared?
- follow
Create stage
We know thatActivation/code execution phase
Before, the variable was created. So when the function is executed,foo
Has been inActive objects
Is defined in. - Foo is declared twice, why is it finally displayed as
function
Rather thanundefined
或string
? * - although
foo
Was declared twice, but fromCreate a stage
We all know that functions are created before variablesActive objects
And if the property name already existsActive objects
Where repeated declarations are ignored. - so
function foo()
The citation was first inActive objects
When the interpreter is encounteredvar foo
, we found thatfoo
The attribute name already exists so the interpreter does nothing and continues to run. - Why is bar undefined ?
bar
It’s actually a variable that’s assigned to a function, and we all know that the variable is inCreate a stage
Create, but they are initialized toundefined.
conclusion
Hopefully now you understand how the JavaScript interpreter executes your code. Understanding the execution context and call stack gives you a clear idea of why your code is executing in a different way than you expected.
Do you think knowing the inner workings of the JS interpreter is too much of a chore or would it help your JavaScript knowledge? Can understanding the stages of execution context help you write better JavaScript code?
Note: Some people have asked me about closures, callbacks, timers, etc., which I will cover in the next article. Read scope chains to learn more about execution context.
Derived from the reading
- ECMA-262 5th Edition
- ECMA-262-3 in detail. Chapter 2. Variable object
- Identifier Resolution, Execution Contexts and scope chains