Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Do ES6 arrow functions still close over "this" even if they don't use it?

I'm trying to understand the rules of when this is lexically bound in an ES6 arrow function. Let's first look at this:

function Foo(other) {
    other.callback = () => { this.bar(); };

    this.bar = function() {
        console.log('bar called');
    };
}

When I construct a new Foo(other), a callback is set on that other object. The callback is an arrow function, and the this in the arrow function is lexically bound to the Foo instance, so the Foo won't be garbage collected even if I don't keep any other reference to the Foo around.

What happens if I do this instead?

function Foo(other) {
    other.callback = () => { };
}

Now I set the callback to a nop, and I never mention this in it. My question is: does the arrow function still lexically bind to this, keeping the Foo alive as long as other is alive, or may the Foo be garbage collected in this situation?

like image 974
cfh Avatar asked Mar 05 '16 11:03

cfh


People also ask

Do arrow functions have closure?

In JavaScript, arrow functions provide a concise syntax for anonymous function expressions stripped off of their OOP baggage. They are a syntactic sugar on a subset of the function capabilities. Both can be used as closures capturing variables of the outer scope.

What is a reason to not use an ES6 arrow function?

An arrow function doesn't have its own this value and the arguments object. Therefore, you should not use it as an event handler, a method of an object literal, a prototype method, or when you have a function that uses the arguments object.

Do arrow functions always return?

Arrow functions allow you to have an implicit return: values are returned without having to use the return keyword. This should be the correct answer, albeit needing a bit more explanation. Basically when the function body is an expression, not a block, that expression's value is returned implicitly.

Does arrow function return value by default?

No, arrow functions do not have to return a value.


1 Answers

My question is: does the arrow function still lexically bind to this, keeping the Foo alive as long as other is alive, or may the Foo be garbage collected in this situation?

As far as the specification is concerned, the arrow function has a reference to the environment object where it was created, and that environment object has this, and that this refers to the Foo instance created by that call. So any code relying on that Foo not being kept in memory is relying on optimization, not specified behavior.

Re optimization, it comes down to whether the JavaScript engine you're using optimizes closures, and whether it can optimize the closure in the specific situation. (A number of things can prevent it.) The situation just like this ES5 example with a traditional function:

function Foo(other) {
    var t = this;
    other.callback = function() { };
}

In that situation, the function closes over the context containing t, and so in theory, has a reference to t which in turn keeps the Foo instance in memory.

That's the theory, but in practice a modern JavaScript engine can see that t is not used by the closure and can optimize it away provided doing so doesn't introduce an observable side-effect. Whether it does and, if so, when, is entirely down to the engine.

Since arrow functions truly are lexical closures, the situations are exactly analogous and so you'd expect the JavaScript engine to do the same thing: Optimize it away unless it causes a side-effect that can be observed. That said, remember that arrow functions are very new, so it may well be that engines don't have much optimization around this yet (no pun).

In this particular situation, the version of V8 I was using when I wrote this answer in March 2016 (in Chrome v48.0.2564.116 64-bit) and still here in January 2021 (Brave v1.19.86 based on Chromium v88.0.4324.96) does optimize the closure. If I run this:

"use strict";
function Foo(other) {
    other.callback = () => this; // <== Note the use of `this` as the return value
}
let a = [];
for (let n = 0; n < 10000; ++n) {
    a[n] = {};
    new Foo(a[n]);
}
// Let's keep a Foo just to make it easy to find in the heap snapshot
let f = new Foo({});

log("Done, check the heap");
function log(msg) {
    let p = document.createElement('p');
    p.appendChild(document.createTextNode(msg));
    document.body.appendChild(p);
}

and then in devtools take a heap snapshot, I see the expected 10,001 instances of Foo in memory. If I run garbage collection (these days you can use the trash can icon; with earlier versions I had to run with a special flag and then call a gc() function), I still see the 10,001 Foo instances:

enter image description here

But if I change the the callback so it didn't reference this:

      other.callback = () => {  }; // <== No more `this`

"use strict";

function Foo(other) {
    other.callback = () => {}; // <== No more `this`
}
let a = [];
for (let n = 0; n < 10000; ++n) {
    a[n] = {};
    new Foo(a[n]);
}
// Let's keep a Foo just to make it easy to find in the heap snapshot
let f = new Foo({});

log("Done, check the heap");
function log(msg) {
    let p = document.createElement('p');
    p.appendChild(document.createTextNode(msg));
    document.body.appendChild(p);
}

and run the page again, I don't even have to force garbage collection, there's just the one Foo instance in memory (the one I put there to make it easy to find in the snapshot):

enter image description here

I wondered if it were the fact that the callback is completely empty that allowed the optimization, and was pleasantly surprised to find that it wasn't: Chrome is happy to retain parts of the closure while letting go of this, as demonstrated here:

"use strict";
function Foo(other, x) {
    other.callback = () => x * 2;
}
let a = [];
for (let n = 0; n < 10000; ++n) {
    a[n] = {};
    new Foo(a[n], n);
}
// Let's keep a Foo just to make it easy to find in the heap snapshot
let f = new Foo({}, 0);
document.getElementById("btn-call").onclick = function() {
    let r = Math.floor(Math.random() * a.length);
    log(`a[${r}].callback(): ${a[r].callback()}`);
};
log("Done, click the button to use the callbacks");

function log(msg) {
    let p = document.createElement('p');
    p.appendChild(document.createTextNode(msg));
    document.body.appendChild(p);
}
<input type="button" id="btn-call" value="Call random callback">

Despite the fact that the callbacks are there and have their reference to x, Chrome optimizes the Foo instance away.


You asked about spec references for how this is resolved in arrow functions: The mechanism is spread throughout the spec. Each environment (such as the environment created by calling a function) has a [[thisBindingStatus]] internal slot, which is "lexical" for arrow functions. When determining the value of this, the internal operation ResolveThisBinding is used, which uses the internal GetThisEnviroment operation to find the environment that has this defined. When a "normal" function call is made, BindThisValue is used to bind the this for the function call if the environment is not a "lexical" environment. So we can see that resolving this from within an arrow function is just like resolving a variable: The current environment is checked for a this binding and, not finding one (because no this is bound when calling an arrow function), it goes to the containing environment.

like image 88
T.J. Crowder Avatar answered Oct 04 '22 16:10

T.J. Crowder