Photo by Matthew Lancaster on Unsplash
I think I wanted to explain the difference between arrow and normal functions, but I kinda explained this and closures by accident
Glossary
A
binding
is a variable name (this I borrow from the previous glossary). Can be classified into one of two categories:A
lexical binding
, which can be determined at the lexing step of the compilation. Any variable or function declaration in user-authored code.A
dynamic binding
, which can only be determined at runtime. Thethis
binding, i.e.
A
normal function
is every kind of function but an arrow one. Okay,async functions
andgenerators
sometimes neither.
What’s this article about?
Well... it’s not about execution contexts
. It’s more about what execution contexts
are not. It assumes you already know... wait, am I not just writing the same as in the previous article? I guess I just don’t know what this article is about. But it aims to go a bit deeper into the this
binding in arrow functions and explores executions context
as an inevitable side-effect. Also, worth mentioning it’s going to go as deep as my current knowledge allows me to... shit, I did it again.
Once I’ve finished writing it, I think I’ve kinda explained something about the difference between arrow and normal functions, this
and closures
.
The misconception
❓ Arrow functions don't define their own
this
. In arrow functions,this
refers to the same value asthis
in their outer lexical scope.
I don't know who first described it that way, but everyone repeats it and it has always bugged me. Not because it's necessarily wrong but because, to me, it's a bad explanation.
First of all, I feel like everyone repeats it but doesn’t understand its meaning.
Second, it kinda makes it feel as if the value of this
in an arrow function could be (or even was!) determined at compile-time.
Yes, in arrow functions, this
refers to the same value as this
in their outer lexical scope. But I have two issues:
To determine
this
in their outer lexical scope, at some point you have to ask yourself what’s the value ofthis
in their outer lexical scope! And there’s a good chance that can’t be determined at compile-time if there’s anynormal function
up in theirscope chain
. So you can’t just look at a representation of thescope chain
of your code (otherwise Bubbl.es would be amazing!) and reason about thethis
keyword. You most likely have to think about run-time. Simply because thethis
keyword is completely pointless in an “only-arrow functions” environment.What’s its “outer lexical scope”, anyway?
I’d like to rephrase it as:
✅ Arrow functions don't bind a value to
this
. In arrow functions, the value ofthis
is resolved through a lexical lookup, as if it was alexical binding
. But beware, the whole lookup might not be entirely lexical.
An example of why I think it matters:
function Vector(x, y) {
this.x = x
this.y = y
}
Vector.prototype.add = function(vector) {
const sum = () => {
console.log(this)
this.x = this.x + vector.x
this.y = this.y + vector.y
}
sum()
}
const v1 = new Vector(0, 0)
const v2 = new Vector(1, 1)
v1.add(v2) // logs { x: 0, y: 0 }
v2.add(v1) // logs { x: 1, y: 1 }
A lexical lookup suggests just by looking at the code arrangement of the function is enough to understand the whole story. But that’s not true.
We can do better
const vector = {
x: 0,
y: 0,
add: (vector) => {
this.x = this.x + vector.x
this.y = this.y + vector.y
}
}
vector.add(/** assuming it's been declared somewhere */ otherVector)
Assuming the snippet above runs in strict mode
and in the global execution context
, running vector.add(otherVector)
will produce a TypeError
since the value of this
will be undefined
. That's because the arrow function is declared when the object is initialized, in the global context, where this
' value is undefined
(again, assuming strict mode
).
The snippet adds up to the misconception quote:
Arrow functions don't define their own
this
, because if it did, it would point tovector
for that particular invocation we have discussed.In arrow functions,
this
refers to the same value asthis
in their outer lexical scope, and since its "outer lexical scope" is the "globalscope
", that's why it'sundefined
in the snippet above (again, instrict mode
).
Is the scope
the same thing as an execution context
, though?
Execution contexts
and scopes
💪🏼 Ok, but what’s an execution context
, anyway?
💡 When a function is invoked, an execution context is created to hold the state necessary to run it.
It’s the set of stuff a function needs to be run, like how to resume execution of the code that invoked it once finished and other information about its surroundings.
🥞 It’s also the thing that gets pushed into the call stack
(I’ll write about the call stack
and, when I do, I’ll make a cross-reference from this blog to that and from that to this and everything will make sense, you’ll see).
⛓️ The scope
(or what we usually mean when we speak about the scope
) is a spec term, but it’s broader meant kinda as a conceptual one.
💡 I like to define the
scope
of a variable as the region of the code where that variable is available. Then I define thescope
(or thescope chain
) as the set of regions that limit the availability of variables.
🧩 But, at least in JS, that concept partially maps to a concrete concept that represents one of the layers of state that compose an execution context
: a function environment record
.
💡 A
function environment record
is some memory space that holds thelexical bindings
available in a particularexecution context
. However, it also holds somedynamic bindings
: mainly, the one that catches our attention is thethis
keyword.
Function environment records
is the way JS controls the availability of variables in a certain execution context
. But also, function environment records
hold a reference to an outer function environment record
(the one that is part of the execution context
that declared the function this function environment record
’s execution context
belongs to). And that resembles the scope chain
a lot! If not for the dynamic bindings
, it’d perfectly map to our concept of scope
.
So an execution context
kinda has a 1:1
relationship with a function environment record
which, at the same time, kinda maps to our concept of scope
. But just kinda.
Can we explain what’s this
?
The
this
keyword is adynamic binding
that belongs to thefunction environment record
of a function’sexecution context
and whose main goal is dynamically accessing different objects in order to reuse code.
Learn more about this
: The rules of this
Wasn’t the misconception related to arrow functions?
Yes, yes! I’m trying to get there! But first, did you know functions are objects? Learn more about it:
So to speak about arrow functions, let’s first deviate again and speak about normal functions
.
Let’s imagine the declarations and invocation of a normal function
called fn
. Here, take a look at some pseudocode:
outerFn() {
...
fn() {
...
}
...
fn()
}
outerFn()
So we run outerFn
, and then...
Declaration of fn
When declared, it’s just the function object
.
Invocation of fn
When invoked, before running its code, we need to know how to.
Thus we create a brand new execution context
for it. Different from the ones previously created for fn
, if it had been previously run. Different from the ones that will be created for fn
if run again. A new one per each invocation.
As discussed, while creating fn
's execution context
, JS will attach a function environment record
to it which will hold uninitialized (thus undefined
) lexical bindings
for all the variables and functions declared inside of the function (hi, hoisting
!). These won’t be out of anywhere, though; remember how they are lexical
and that has something to do with compiling? Yep, JS knows well what those binding
names are.
Also from the get-go, every function declaration
declared in it will be initialized, so a function object
will be created for every function declaration
that fn
contains (hi again, hoisting
—now with initialization of function declarations
because the spec says so—). I briefly mentioned it at the end of:
Hoisting without mentioning code rearrangement
As discussed too, that function environment record
won't only hold those bindings, but also has a reference to an outer function environment record
: the function environment record
that belongs to the execution context where fn
was declared. We can even give it a name: in the above code snippet, when we ran outerFn
, an execution context
just like the one we’re describing for fn
was created for it. And outerFn
's execution context
, just like fn
's, has its own function environment record
, which will be fn
’s function environment record
outer function environment record
. Don’t forget about outerFn
's function environment record
, because we will come back to it soon!
⚠️ Oh, a minor detail! If
outerFn
returned
fn
, orscheduled
fn
's execution for a later time whenouterFn
was already finished, thus out of thecall stack
,fn
, when run, would still have access to the variables declared byouterFn
becausefn
'sfunction environment record
would still have access toouterFn
'senvironment record
.Yes, that’s
closures
without magic. Oh, did I say minor? Sorry, it’s actually a huge detail.
As discussed too (the last one, I promise), that function environment record
won't only hold lexical bindings
, but also some dynamic bindings
(hi, this
among others!). So the value of this
will potentially be different every time fn
is executed depending on how it's executed. It may have been called using Function#call
or Function#apply
. It may be bound through Function#bind
. It may be a property anywhere in the prototype chain of an object and called through it.
What if fn
was an arrow function?
Same stuff, just a couple of differences here and there:
When thinking about arrow functions as
function objects
, their[[Construct]]
slot is empty.Arrow functions never bind a value to their
function environment record
'sdynamic bindings
: northis
, norsuper
, norarguments
(who’s that sucker anyway?). Instead, the resolution algorithm for these is a lexical lookup, as in anylexical bindings
, through thescope chain
(orfunction environment records
chain), all the way up to theglobal execution context
, only stopping if finding a binding they can resolve to in their way. I told you to rememberouterFn
'sfunction environment record
, didn’t I? That’s where the lookup will start. The mechanism that explainsclosures
and howthis
is resolved in arrow functions is inherently... the same!
The [[Construct]]
bit also holds true for async functions
and generators
, while thethis
binding doesn't (unless... you know... they are also arrows).