JavaScript execution context — scope chain, closure, and this (part 4)

Carson
8 min readFeb 28, 2020

Many find the following concepts are the complicated part of JavaScript:

  • scope chain
  • closure
  • this

These concepts are more comfortable to understand than they look like, especially with the knowledge of the execution context.

What are these three concepts in common? They all relate to the variable lookup, the way the JavaScript engines looks for a variable.

Variable lookup

A variable lookup could be confusing in the following example.

When executing the isApple function, we have three stacked execution contexts in position:

  • the global execution context
  • the isBananafunction execution context
  • the isApplefunction execution context

Next, the console begins looking up the apple variable.

Intuitively, we may analyze the chain lookup by following a top-to-bottom flow in the call stack. The console would log “banana” because it finds the apple variable in the isBanana function execution context.

By contrast, the console actually logs “apple” assigned in the global execution context.

Why?

Outer

Our chain lookup missed a critical component in the execution context, the outer.

The outer defined how the JavaScript engine performs the chain lookup, also known as the scope chain.

If we look into the isApple execution context, its outer points to the global execution context.

In this case, the JavaScript engine looks for an apple variable in the global execution context immediately after failing to find it in the isApple execution context.

Did the mystery resolve?

Not really. The outer concept leads to another question.

Why the outer of isApple execution context points to the global one instead of the isBanana?

After all, the isApple function is called inside of the isBanana. Shouldn’t the scope chain follow the call stack?

Counterintuitively, JavaScript’s scope chain is defined by the lexical scope and never influenced by the call stack.

From the two-step process perspective, the scope chain is defined at the compiling step, not the execution step.

To further answer this question, we need to demystify how JavaScript designs its lexical scope.

Lexical scope

JavaScript engine has a rule: the lexical scope is defined by where the function located.

Let’s take a look at the same example from a lexical scope perspective.

In this case, the isApple and isBanana functions are declared in the global scope. Therefore, their lexical scopes are the global scope.

When the JavaScript engine compiles the script, the outers in both function execution contests are pointed to the global execution context.

To better understand this feature, let’s take a look at another example. Instead of declaring functions in the global scope, we indent each function inside of the previous one.

In this case,

  • function priceA is defined in the global scope;
  • function priceB is defined in the priceA scope;
  • function priceC is defined in the priceB scope.

Based on lexical scopes, we can reason the outer in each execution context:

  • In priceC execution context, the outer points to priceB execution context;
  • In priceB execution context, the outer points topriceA execution context;
  • In priceA execution context, the outer points to the global execution context.

At the end of the execution, the console logs “30.”

That’s how the scope chain works in the JavaScript execution context.

Closure

The closure is more straightforward to understand than it sounds. Let’s take a look at an example.

We have the following call stack right before the util is returned and assigned to the price variable.

After returning the util, the applePrice function execution ends, and its execution context is removed.

Meanwhile, variable and lexical environments disappear, and variables inside of them are supported to be destroyed.

At this moment, JavaScript’s lexical scope rule kicks in — an inner function can always access to variables in its outer function.

Here, the inner functions are getPrice and setPrice, and the outer function is applePrice.

The getPrice function uses two variables, fruit and price, while setPrice function uses the price.

Following the rule, fruit and price variables are kept in a separate area. It is an exclusive area where can only be accessed by getPrice and setPrice function, also known as the closure.

Meanwhile, the discount variable is destroyed because no methods hold a reference to it.

Next, the execution continues and calls the setPrice function. The JavaScript engine looks through the scope chain and locates the price variable in the closure. The value of the price is set to “20.”

In the last line, the getPrice is called. Following the same chain lookup, the JavaScript engine finds the fruit and price variables in the closure and logs out “apple” and “20” correspondingly.

The execution ends.

By running the example codes in your Chrome, you can see the closure in its dev tools.

“This” is not part of a scope chain

We have touched three components in an execution context:

  • variable environment
  • lexical environment
  • outer

The last one is this.

Each scope has it’s this.

If we log this in the global scope, we receive a window object.

The window is the only element where this and scope concept joins because it is part of the global scope located at the root end of the scope chain.

How about the this in a function scope?

Is this referred to the applePrice function?

Interestingly, the console logs the window object, the same as it runs in the global scope.

this is not related to any scope concepts.

But who is this? Is it always the window object?

Who is “this”?

Let’s take a look at an example.

In this example, the getPrice logs “10”, and getThis logs the apple object.

So, we found the answer: whoever calls the method is this.

While the outer defined at the compiling step, this is determined at the execution step.

When a function declared, it is attached to the window object. When you execute a function, it is the window object which calls the function. Consequently, this is the window object.

We can reset the this by changing the caller.

In the last line, we use the call function to change the this to the banana object.

When the JavaScript engine executes this line, it is the banana object calls the getPrice function. Hence, this is banana, and the console logs “20”.

Convert this to the scope concept

Though the this has nothing to do with the scope, we can easily convert it to the scope concept.

The following example shows a typical gotcha moment in using this.

Who calls the discount function?

At a glance, it looks like the getPrice calls it. However, the console logs the window object.

So far, we know a function (or method) is either called by an object or the window, not by a function. In this case, it is the window calls the discount.

It is a design flaw of JavaScript — the this doesn’t inherit from the outer scope because it has never been a part of the scope concept.

We can quickly fix this issue by assigning this to a local variable.

By doing so, the scope chain starts working.

Since ES6, we have the arrow function to avoid using a redundant self variable.

An arrow function doesn’t convert the this to a scope concept. Instead, it simply doesn’t create an execution context and shares the same this of the method.

What are the takeaways?

  • The outer defined the variable chain lookup, AKA, the scope chain.
  • The lexical scope defines the outer, and where you write functions sets the lexical scope.
  • The scope chain is determined at the compiling step, not the execution step. Hence, a function call, which happens at the execution step, doesn’t affect the scope chain.
  • The closure appears because of the lexical scope rule — an inner function can always access to variables in its outer function. It is exclusive to the functions holding the variable references.
  • this is not a scope concept. Whoever calls the function (or method) is this.

Further reading

  • This is a post about the execution context. You can find more information about the outer.
  • If you are interested in some extensive reading, this post is talking beyond the JavaScript call stack.

--

--