当引用在 Javascript 中脱离上下文时,按引用捕获是否会变成按值捕获?

Does capture by reference turn into capture by value when the reference goes out of context in Javascript?

以下Javascript程序:

function f() {
  function g() { console.log(x); }
  let x = 0;
  g();  // prints 0
  x = 1;
  g();  // prints 1
  return g;
}

let g = f();
g();  // prints 1

输出:

0
1
1

所以似乎 g 首先通过引用 捕获 x (因为在 fg() 中打印 0 then 1 when x is rebound),这意味着g闭包环境看起来像{'x': x},然后by value(因为在 f 之外,当 xf 正文末尾脱离上下文时,g() 打印 1),这意味着 g 闭包环境看起来像 {'x': 1}.

我试图将此行为与 C++ lambda 相关联,后者提供按引用和按值捕获,但与 Javascript 相反,不允许按引用捕获通过转换为超出引用范围按值捕获(相反,调用 lambda 成为未定义的行为)。

Javascript 捕获的解释是否正确?

如果该解释是正确的,那将清楚地解释 块作用域 变量 (let) 的捕获如何在 for 循环中工作:

let l = [];

for (let x = 0; x < 3; ++x) {
  l.push(function () { console.log(x); });
}

l[0]();  // prints 0
l[1]();  // prints 1
l[2]();  // prints 2

在 JavaScript 中,当 g() 引用表达式中的变量 x 时,无论 g() 是否从 f() 内部调用,实际上都没有区别。只有一个变量 x,每当 g() 的代码运行时获取它都是相同的内部操作。

JavaScript 与 C++ 有很大的不同;表面上的相似性可能具有欺骗性。此外,在 讨论 JavaScript 语义时很少使用术语“捕获”(根据我的经验,例如在 Stack Overflow 上),尽管规范在其详尽描述中使用了它进入范围时发生。这里的相关词是闭包,如“x 在 g() 的闭包中。(我对术语很草率,所以有人可能会改进我的措辞。)

更多:注意我们可以修改g()来证明x仍然不仅可以访问获取其值,还可以修改:

    function f() {
      function g() { console.log(x = x + 1); }
      let x = 0;
      g();  // prints 1
      x = 1;
      g();  // prints 2
      return g;
    }
    
    g = f();
    g();
    g();
    g();

变量 x 的行为与普通变量的行为始终如一。

简而言之

你几乎是正确的,除了当它超出范围时它是如何工作的。

更多详情

如何在 JavaScript 中“捕获”变量?

JavaScript 使用 lexical environments to determine which function uses which variable. Lexical environments are represented by environment records。你的情况:

  • 有全局环境;
  • 函数f()定义了它的词法环境,其中定义了x,即使是在g();
  • 之后
  • 内部函数g()定义了它的词法环境,它是空的。

所以 g() 使用 x。由于那里没有 x 的绑定,因此 JavaScript 在封闭环境中查找 x。既然在里面找到了,那么g()中的x就会使用f()中的x的绑定。这看起来像 lexically scoped 绑定。

如果稍后在调用 g() 的环境中定义 xg() 仍将绑定到 f() 中的 x

function f() {
  function g() { console.log(x); }
  let x = 0;
  g();  // prints 0
  x = 1;
  g();  // prints 1
  return g;
}

let x = 4;
let g = f();
g();  // prints 1 (the last known value in f before returning)

Online demo

这表明绑定是静态的,并且将始终引用在定义 g() 的词法范围内已知的 x

这个 excellent article 详细解释了它是如何工作的,图形非常漂亮。它适用于闭包(即具有执行上下文的匿名函数),但也适用于普通函数。

为什么会保留超出范围的变量值?

如何解释这种非常特殊的行为,只要 x 仍在范围内(就像 C++ 中的引用),JavaScript 将始终采用 x 的当前值,而它当 x 超出范围时(当 C++ 中的超出范围引用将是 UB 时)将采用最后一个已知值?当变量消失时,JavaScript 是否将值复制到闭包中?不,比这还简单!

这与garbage collection有关:g()返回到外部上下文。由于g()使用了f()中的x,垃圾收集器会意识到f()的这个x对象仍在使用中。因此,只要 g() 可访问,f() 中的 x 将保持活动状态并保持可访问,以便其仍然处于活动状态的绑定。因此无需复制值:x 对象将保持不变(未修改)。

作为不是抄袭的证明,你可以研究下面的代码。它在 f() 的上下文中定义了第二个函数,该函数能够更改(相同的)x:

let h;

function f() {
  function g() { console.log(x); }
  h = function () { x = 27; }
  let x = 0;
  g();  // prints 0
  x = 1;
  g();  // prints 1
  x = 3;
  return g;
}

let x = 4;
let g = f();
g();  // prints 3
h();
g();  // prints 27

Online demo

编辑: 附加 bonus article 在稍微复杂的上下文中解释了这种现象。有趣的是,它解释说如果不采取预防措施,这种情况会导致内存泄漏。