What Is the 'Execution Context' in JavaScript Exactly

What is the 'Execution Context' in JavaScript exactly?

You're asking about several different concepts that aren't very closely related. I'll try to briefly address each.


Execution context is a concept in the language spec that—in layman's terms—roughly equates to the 'environment' a function executes in; that is, variable scope (and the scope chain, variables in closures from outer scopes), function arguments, and the value of the this object.

The call stack is a collection of execution contexts.

See also this answer and this article.


Scope is literally that: the scope in which a variable can be accessed. Simplistically:

var x;

function a() {
var y;
}

x can be accessed from anywhere. When a is invoked, x will be in the outer scope. (Stored in the scope chain.)

In contrast, y can only be accessed by code in a() because it is limited to a's scope. This is what the var keyword does: restricts a variable to the local scope. If we omitted var, y would end up in the global scope, generally considered a bad thing.


Think of hoisting as more of a compile-time thing. In JavaScript, function declarations are "hoisted" to the top of their scope. In other words, they are parsed and evaluated before any other code. (This is opposed to function expressions, which are evaluated inline.) Consider the following:

a();
b();

function a() { }
var b = function() { }

The call to a() will succeed because its declaration was hoisted to the top; a was assigned to automatically before program execution began. The call to b() will fail with a TypeError because b will not be defined until line 4.

Invocation context and execution context in javascript: are we talking of the same thing?

The two terms are closely related but not the same thing.

In a nutshell they define scope vs. context. Scope is about the environment that code runs in (kind of like a room - - it's about where the code is) and context is about an actual object that caused some code to be executed (like who was responsible for putting you in that room).

An "execution context" refers to the "scope chain" that is in
effect when some code is running. A scope chain is a list of
memory locations that should be checked (in a particular order) for
identifiers (variable, constant and function names) to be resolved to
a value. Since JavaScript is executed in a single-threaded
environment, only one task can be executed at a time. The currently
executing code (and its associated scope) define the execution
context.

A simple example can be shown like this:

// This area is in the Global execution context (scope) because the code is 
// not wrapped in a function or any other kind of code block.
var x = "Global";

// "Global" is the result because the JavaScript engine will always look
// in the current scope for a declaration for the identifier in question.
// It will find a declaration for "x" right here in the Global scope, so
// that's the value it will use.
console.log(x);

var y = "Also Global";

function parent(){
// This area is in the "parent" execution context (scope)
var x = "parent";

// "parent" is the result (not "Global") because when this function is
// executing, its scope is the most accessible. The JavaScript engine
// looks here first to find out what "x" is. This is known as variable
// "hiding" because the x in the parent scope hides the x in the Global scope.
console.log(x);

function child() {
// This area is in the "child" execution context (scope)
var x = "child";

// "child" is the result (not "Global" or "parent") because when this
// function is executing, its scope is the most accessible. The
// JavaScript engine looks here first to find out what "x" is. This
// x now hides the x in parent, which is hiding the x in Global.
console.log(x);

// "Also Global" is the result here. First the current execution
// context (scope) is checked for a "y" variable. There isn't one,
// so the next scope in the scope chain (function parent) is checked.
// There is no "y" declared there either. So, again, the next highest
// scope in the chain (Global) is checked and that is where "y" is
// found, so the value of that "y" is used.
console.log(y);

// Here, we will get "undefined". All the scopes in the chain will
// be checked and if we go all the way up to Global and still don't
// find a declaration for "z", there is no other scope to look in.
console.log(z);
}
child();
}

parent();

Why is execution context needed for JS

The concept of an "execution context" provides a way of reasoning about what happens when the global environment is created and executed, or when a function is called. It's a conceptual container for the local variables (globals, in the case of the global environment), parameters (for functions), etc. A JavaScript engine can be written without literally having any object it calls an "execution context," as long as it implements the language in keeping with the spec's defined execution context behavior.

One thing that the execution context helps explain is the behavior of closures. A function created within a given execution context has (conceptually) a reference to that context, even after the function call related to the context has completed:

function foo(answer) {
return function say() {
console.log(answer);
};
}
const s = foo(42);
s(); // 42

That works because the function say has a reference to the context of the call to foo that created it (well, more specifically to the thing called its "lexical environment"). That lexical environment continues to exist after foo returns because something still has a reference to it (say). So the call to say afterward works.

The reason for having two phases is to allow use of identifiers before their declaration. This is primarily useful for function declarations:

main();

function main() {
console.log("Worked");
}

The first phase processes the function declarations (and var statements), and then the step-by-step phase runs the code. Without the first phase, the code above would fail as of main(); because main would be undeclared. Of course, with the simple example above, you could just move the main(); call to after the function declaration, but more complicated cases become harder to solve in that way. Languages that didn't have two phases (early C, for instance) had to provide a mechanism for "forward declaring" things that would be defined later. Having two phases means JavaScript doesn't have to have those. (To be fair, C also differs from JavaScript in that it needs to know what all identifiers refer to during compilation, even if the identifiers are in code within functions. So it needed forward declarations just to allow foo and bar to call one another. JavaScript doesn't check the identifiers used within functions until the function is called, so some of the reasons for forward declarations in C wouldn't come up in JavaScript even if it didn't have two phases.)

It wasn't perfectly successful. Having var statements initialize the variables they declare with undefined before the var statement is reached in the code was often the source of bugs and confusion:

console.log(answer); // undefined
var answer = 42;

It's easy for people to be confused by the fact that half of the var answer = 42; was done early (the var answer part), but the other half (answer = 42;) isn't done until later when that statement is reached.

That's why let and const create, but don't initialize, their variables during that first phase. You can use a variable above where it's declared, but only in code that runs after the initialization:

function foo() {
console.log(answer);
}
// foo(); <== Would fail, `answer` isn't initialized yet
let answer = 42;
foo(); // Works, logs 42

Does javascript create an execution context when we call web api functions?

Yes, all function call result in an execution context being created.

First of all, a tiny bit of background to make the subsequent explanations easier to understand. The spec talks about internal slots, these are abstract properties and methods that must be implemented. They aren't exposed to the outside, so you cannot use them directly, they for the implementation. The spec uses names that are in double square brackets, so for example [[Name]] - that refers to an internal slot called "name", for example.

With this in mind, let's also clarify what is a function according to the spec (scroll to the bottom of section 6.1.7.2):

Table 7 summarizes additional essential internal methods that are supported by objects that may be called as functions. A function object is an object that supports the [[Call]] internal method. A constructor is an object that supports the [[Construct]] internal method. Every object that supports [[Construct]] must support [[Call]]; that is, every constructor must be a function object. Therefore, a constructor may also be referred to as a constructor function or constructor function object.

Table 7 then describes more abstractly what [[Call]] and [[Construct]] do. At any rate, in short, it's an object that has the internal slot [[Call]].

Finally, we can have a look at [[Call]]. Quoting from the spec of chapter 9.2 ECMAScript Function Objects from chapter on how [[Call]] should work:

9.2.1 [[Call]] ( thisArgument, argumentsList )

The [[Call]] internal method for an ECMAScript function object F is called with parameters thisArgument and argumentsList, a List of ECMAScript language values. The following steps are taken:

  1. Assert: F is an ECMAScript function object.
  2. If F.[[IsClassConstructor]] is true, throw a TypeError exception.
  3. Let callerContext be the running execution context.
  4. Let calleeContext be PrepareForOrdinaryCall(F, undefined).
  5. Assert: calleeContext is now the running execution context.
  6. Perform OrdinaryCallBindThis(F, calleeContext, thisArgument).
  7. Let result be OrdinaryCallEvaluateBody(F, argumentsList).
  8. Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
  9. If result.[[Type]] is return, return NormalCompletion(result.[[Value]]).
  10. ReturnIfAbrupt(result).
  11. Return NormalCompletion(undefined).

This is the full section and it applies to "normal" functions - those that are created by you or me. The full set of steps is actually irrelevant, I've only included it for completeness sake. The important part is step 4. - I will NOT quote the entirety of PrepareForOrdinaryCall because it's even more steps that don't really concern us, except these ones:


  1. Let calleeContext be a new ECMAScript code execution context.
  2. Push calleeContext onto the execution context stack; calleeContext is now the running execution context.
  3. Return calleeContext.

So, in short, the function gets a new running execution context and that is put onto the stack. OK. Then at step 8. of [[Call]] the execution context is removed from the stack.


Now, let's have a look at section 9.3 Built-in Function Objects. These are function objects that are provided by the environment and conform to the ECMAScript spec, things like Object, Array, parseInt, etc. The spec allows for these built-in functions to be implemented in the language itself (not necessarily JavaScript, remember that ECMAScript is the standard, so it would be the language implementing the standard) or as provided to the environment, which means potentially implemented in a different language. Still, the spec says:

If a built-in function object is not implemented as an ECMAScript function it must provide [[Call]] and [[Construct]] internal methods that conform to the following definitions:

So, even if provided, the functions still have to provide a [[Call]] slot. And here is the behaviour it should follow:

9.3.1 [[Call]] ( thisArgument, argumentsList )

The [[Call]] internal method for a built-in function object F is called with parameters thisArgument and argumentsList, a List of ECMAScript language values. The following steps are taken:

  1. Let callerContext be the running execution context.
  2. If callerContext is not already suspended, suspend callerContext.
  3. Let calleeContext be a new execution context.
  4. Set the Function of calleeContext to F.
  5. Let calleeRealm be F.[[Realm]].
  6. Set the Realm of calleeContext to calleeRealm.
  7. Set the ScriptOrModule of calleeContext to F.[[ScriptOrModule]].
  8. Perform any necessary implementation-defined initialization of calleeContext.
  9. Push calleeContext onto the execution context stack; calleeContext is now the running execution context.
  10. Let result be the Completion Record that is the result of evaluating F in a manner that conforms to the specification of F.thisArgument is the this value, argumentsList provides the named parameters, and the NewTarget value is undefined.
  11. Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
  12. Return result.

Again, the full set of steps is irrelevant. The important ones are 3., 9., and 11. - a new execution context is created, pushed to the stack, and finally removed. Step 8. allows for any implementation specific changes to the execution context.


These are the sections that prescribe how running a function should work. In both cases a new execution context is created, put on the stack and then removed once the function finishes.

For completeness, here is chapter 8.3 . Execution Contexts if you want to know more about them. To summarise the important parts, there is at most one running execution context at any one point per thread. The running execution stack is also defined there as a LIFO structure with the currently running execution context on top.



Related Topics



Leave a reply



Submit