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?
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.
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.
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.
No, arrow functions do not have to return a value.
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:
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):
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With