29

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

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

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

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

如果我这样做会发生什么?

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

现在我将回调设置为 nop,我从未this在其中提及。我的问题是:箭头函数是否仍然在词法上绑定,只要活着就this保持活着,或者在这种情况下可能会被垃圾收集?FoootherFoo

4

1 回答 1

48

我的问题是:箭头函数是否仍然在词法上绑定到这个,只要其他人还活着就保持 Foo 活着,或者在这种情况下 Foo 可能会被垃圾收集?

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

再优化,归结为你使用的JavaScript引擎是否对闭包进行了优化,具体情况下能否对闭包进行优化。(有很多事情可以阻止它。)就像这个带有传统函数的 ES5 示例一样:

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

在这种情况下,函数关闭包含 的上下文t,因此理论上,有一个引用,t而该引用又将Foo实例保存在内存中。

这就是理论,但在实践中,现代 JavaScript 引擎可以看到t闭包没有使用它,并且可以对其进行优化,前提是这样做不会引入可观察到的副作用。是否会,如果会,何时会,完全取决于引擎。

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

在这种特殊情况下,我在 2016 年 3 月(在 Chrome v48.0.2564.116 64 位中)和 2021 年 1 月(Brave v1.19.86 基于 Chromium v​​88.0.4324)编写此答案时使用的 V8 版本。 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在箭头函数中解析的规范参考:该机制遍布整个规范。每个环境(例如调用函数创建的环境)都有一个[[thisBindingStatus]]内部槽,"lexical"用于箭头函数。在确定 的值时this,使用内部操作ResolveThisBinding,它使用内部GetThisEnviroment操作来查找已this定义的环境。当进行“正常”函数调用时,如果环境不是环境,BindThisValue则用于绑定函数调用。所以我们可以看到,从箭头函数中解析就像解析变量一样:检查当前环境是否存在this"lexical"thisthis绑定并且没有找到一个(因为this调用箭头函数时没有绑定),它进入包含环境。

于 2016-03-05T11:18:31.210 回答