JavaScript execution context — scope chain, closure, and this (part 4)
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
isBanana
function execution context - the
isApple
function 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 thepriceA
scope; - function
priceC
is defined in thepriceB
scope.
Based on lexical scopes, we can reason the outer in each execution context:
- In
priceC
execution context, the outer points topriceB
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) isthis
.