29

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?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
cfh
  • 4,576
  • 1
  • 24
  • 34
  • 3
    Given you don't reference `this`, I doubt that the JS engine would keep a reference to the `Foo` instance, however I don't have any proof for it. – Qantas 94 Heavy Mar 05 '16 at 11:13
  • Wouldn't it depend on which engine runs this code? Or is garbage collection behaviour in the specs? – Kyll Mar 05 '16 at 11:16
  • I guess, it just acting like a closure. `function test(){ var x = 10; return function(){ console.log(x); } }` – Rajaprabhu Aravindasamy Mar 05 '16 at 11:17
  • I'd recommend `()=>{ this; }`just mentioning this, doesn't have to do anything, if you want to take care this instance still sticks around. But why would you want that? Why would you want this instance to stay, if there's no other reference around so you (or any other part of your code) can access/use it? – Thomas Mar 05 '16 at 11:20
  • @Thomas: Actually, I want the opposite: I want to make sure that such functions don't keep a garbage `Foo` around, and I don't know if I can rely on the JS engine to do the right thing or if I should use standard anonymous functions instead. – cfh Mar 05 '16 at 11:23
  • @cfh "I want to make sure that such functions don't keep a garbage" --- you cannot by 2 reasons: 1. the standard does not require to only capture the variables that are actually referenced. 2. the standard does not define the "garbage" term at all. Both statements are valid for both old-style anonymous functions or the fat-arrow based ones. – zerkms Mar 05 '16 at 11:25
  • Closely related: [garbage collection for closed-over variables](https://stackoverflow.com/questions/5326300/garbage-collection-with-node-js) – Bergi May 21 '18 at 12:18

1 Answers1

48

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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • That would be the expectation, but since `this` is in some sense magic, I was wondering if the exact same rules as for normal closures apply. (I didn't downvote this, ftr.) – cfh Mar 05 '16 at 11:20
  • @cfh: `this` isn't magic. :-) The specification is clear about exactly how `this` works, including how arrow functions close over it. The mechanism is exactly the same as for other variables in the context. – T.J. Crowder Mar 05 '16 at 11:23
  • That's good to know. Could you provide a reference to the relevant part of the spec? – cfh Mar 05 '16 at 11:25
  • 1
    @cfh: It's all over, and the spec is very hard to read, but I've added some relevant sections above. – T.J. Crowder Mar 05 '16 at 11:36
  • Thanks. This is a good theoretical answer based on the spec. Unfortunately, it seems my question can't be fully answered in general since what exactly is closed over is implementation defined. – cfh Mar 05 '16 at 11:46
  • And just as I wrote that, you added some tests! The difference to @georg's results is interesting. – cfh Mar 05 '16 at 11:48
  • @cfh: What's *closed over* isn't implementation-defined, but *is* implementation-optimized. :-) – T.J. Crowder Mar 05 '16 at 11:48
  • That seems like nit picking; if the closure over some variable is optimized away because it's not used, can't we say that the variable is not closed over? My terminology may be off here because I'm not familiar with the spec. – cfh Mar 05 '16 at 11:51
  • @cfh: Yes, definitely nit-picking, sorry. I kinda meant to convey that with the smile at the end, didn't come off apparently. :-) – T.J. Crowder Mar 05 '16 at 11:55
  • 3
    This is the very model of a SO answer, thanks for your efforts. Accepting this now. – cfh Mar 05 '16 at 16:45
  • I will mention that, after playing around with my actual app in the heap profiler, it did _seem_ that certain empty callbacks still retained a `this` closure which prevented collection (same Chrome version as you). So, as you said, there seem to be circumstances which break this optimization, though I have no idea what they might be. – cfh Mar 05 '16 at 16:49