即使不使用 ES6 箭头函数,它仍然会关闭 "this" 吗?

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

我试图理解 this 在 ES6 箭头函数中词法绑定的规则。先来看这个:

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

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

当我构造一个 new Foo(other) 时,在另一个对象上设置了一个回调。回调是一个箭头函数,箭头函数中的this词法绑定到Foo实例,所以Foo即使我不保留也不会被垃圾回收Foo 周围的任何其他引用。

如果我这样做会怎样?

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

现在我把回调设置为nop,我在里面再也没有提到this我的问题是: 箭头函数是否仍然在词法上绑定到 this,只要 other 还活着,就保持 Foo 活着,或者Foo 在这种情况下会被垃圾回收吗?

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?

就规范而言,箭头函数引用了创建它的环境对象,并且该环境对象具有 this,而 this 指的是 Foo 该调用创建的实例。因此,任何依赖于 Foo 未保存在内存中的代码都依赖于优化,而不是指定的行为。

再优化,就看你用的JavaScript引擎有没有优化闭包,在具体情况下能不能优化闭包。 (有很多事情可以阻止它。)情况就像这个带有传统功能的 ES5 示例:

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

在那种情况下,该函数会关闭包含 t 的上下文,因此在理论上,它引用了 t,后者又将 Foo 实例保留在内存中。

这是理论,但在实践中,现代 JavaScript 引擎可以看到 t 未被闭包使用,并且可以优化它,前提是这样做不会引入可观察到的一面 -影响。是否会发生,如果发生,何时发生,完全取决于引擎。

由于箭头函数确实是词法闭包,情况完全类似,所以您会期望 JavaScript 引擎做同样的事情:优化它,除非它会导致副作用观测到的。也就是说,请记住箭头函数 非常新,因此引擎很可能还没有对此进行太多优化 (没有双关语).

在这种特殊情况下,我在 2016 年 3 月写这个答案时使用的 V8 版本(在 Chrome v48.0.2564.116 64 位中)并且在 2021 年 1 月仍然在这里(Brave v1 .19.86 基于 Chromium v​​88.0.4324.96) 确实 优化了闭包。如果我 运行 这个:

"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);
}

然后在 devtools 中拍摄堆快照,我在内存中看到预期的 10,001 个 Foo 实例。如果我 运行 垃圾收集(现在你可以使用垃圾桶图标;在早期版本中我必须 运行 使用特殊标志然后调用 gc() 函数),我仍然看到10,001 Foo 个实例:

但如果我更改回调,它就不会引用 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);
}

和 运行 页面,我什至不必强制垃圾收集,内存中只有一个 Foo 实例(我放在那里以便于查找的那个在快照中):

我想知道是不是因为回调完全空了才允许优化,惊喜地发现事实并非如此:Chrome很高兴在放弃 this 的同时保留部分闭包,如下所示:

"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">

尽管回调在那里并引用了 x,Chrome 优化了 Foo 实例。


您询问了有关如何在箭头函数中解析 this 的规范参考:该机制遍布整个规范。如果环境不是 "lexical" 环境,则每个 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 用于绑定函数调用的 this。所以我们可以看到从箭头函数中解析 this 就像解析变量一样:检查当前环境是否存在 this 绑定,但没有找到(因为没有 this调用箭头函数时绑定),它会转到包含环境。