I think I wanted to explain the difference between arrow and normal functions, but I kinda explained this and closures by accident

·

9 min read

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. The this binding, i.e.

  • A normal function is every kind of function but an arrow one. Okay, async functions and generators 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 as this 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 of this in their outer lexical scope! And there’s a good chance that can’t be determined at compile-time if there’s any normal function up in their scope chain. So you can’t just look at a representation of the scope chain of your code (otherwise Bubbl.es would be amazing!) and reason about the this keyword. You most likely have to think about run-time. Simply because the this 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 of this is resolved through a lexical lookup, as if it was a lexical 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 to vector for that particular invocation we have discussed.

  • In arrow functions, this refers to the same value as this in their outer lexical scope, and since its "outer lexical scope" is the "global scope", that's why it's undefined in the snippet above (again, in strict 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 the scope (or the scope 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 the lexical bindings available in a particular execution context. However, it also holds some dynamic bindings: mainly, the one that catches our attention is the this 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 a dynamic binding that belongs to the function environment record of a function’s execution context and whose main goal is dynamically accessing different objects in order to reuse code.

Learn more about this: The rules of this

Yes, yes! I’m trying to get there! But first, did you know functions are objects? Learn more about it:

Function objects

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, or

  • scheduled fn's execution for a later time when outerFn was already finished, thus out of the call stack, fn, when run, would still have access to the variables declared by outerFn because fn's function environment record would still have access to outerFn's environment 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's dynamic bindings: nor this, nor super, nor arguments (who’s that sucker anyway?). Instead, the resolution algorithm for these is a lexical lookup, as in any lexical bindings, through the scope chain (or function environment records chain), all the way up to the global execution context, only stopping if finding a binding they can resolve to in their way. I told you to remember outerFn's function environment record, didn’t I? That’s where the lookup will start. The mechanism that explains closures and how this 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).