Javascript中变量是如何分配内存的?

IT技术 javascript memory-management
2021-01-16 06:25:23

我想知道如何在 javascript 中为局部变量分配内存。在 C 和 C++ 中,局部变量存储在堆栈中。在javascript中是一样的吗?或者一切都存储在堆中?

2个回答

它实际上是 JavaScript 一个非常有趣的领域,至少有两个答案:

  • 关于规范定义的答案,以及
  • 关于 JavaScript 引擎实际做什么的答案,可以优化(通常是)

在规范方面:JavaScript 处理局部变量的方式与 C 的方式完全不同。当您调用一个函数时,会为该调用创建一个词法环境,其中包含称为环境记录的东西为了简单起见,我将它们一起称为“绑定对象”(不过,它们在规范中分开是有充分理由的;如果您想更深入地了解它,请留出几个小时并通读规范)。绑定对象包含函数参数的绑定、函数中声明的所有局部变量以及函数内声明的所有函数(以及其他一些东西)。一个绑定是名称(如a)和绑定的当前值的组合(以及我们在这里不需要担心的几个标志)。函数内的非限定引用(例如,fooin foo,但不是fooin obj.foo,它是限定的)首先针对绑定对象进行检查,以查看它是否与绑定对象匹配;如果是,则使用该绑定。当闭包在函数返回后幸存下来(这可能有多种原因),该函数调用的绑定对象会保留在内存中,因为闭包在创建它的地方有一个对绑定对象的引用。因此,在规范方面,一切都与对象有关。

乍一看,这表明堆栈不用于局部变量;事实上,现代 JavaScript 引擎非常聪明,并且可能(如果值得的话)将堆栈用于闭包实际上并未使用的局部变量。他们甚至可以将堆栈用于确实被闭包使用的局部变量,但是当函数返回时将它们移动到绑定对象中,以便闭包继续访问它们。(当然,堆栈仍然用于跟踪返回地址等。)

下面是一个例子:

function foo(a, b) {
    var c;

    c = a + b;

    function bar(d) {
        alert("d * c = " + (d * c));
    }

    return bar;
}

var b = foo(1, 2);
b(3); // alerts "d * c = 9"

当我们调用 时foo,将使用这些绑定创建一个绑定对象(根据规范):

  • ab ——函数的参数
  • c — 在函数中声明的局部变量
  • bar — 在函数内声明的函数
  • (……还有其他几件事)

foo执行语句c = a + b;,它的引用ca以及b该呼叫在绑定对象上绑定foofoo返回对其中bar声明函数的引用时,bar在调用foo返回时仍然存在由于bar对 的特定调用具有对绑定对象的(隐藏)引用foo,因此绑定对象仍然存在(而在正常情况下,不会有对它的未完成引用,因此它可用于垃圾收集)。

稍后,当我们调用 时bar,会为该调用创建一个新的绑定对象,其中包含(除其他外)一个名为 的绑定d — 的参数bar该新绑定对象获得一个绑定对象:附加到bar. 它们一起形成了一个“作用域链”。bar首先针对该调用的绑定对象检查其中的非限定引用bar,例如,d解析d为调用的绑定对象上的绑定bar但是,然后会根据作用域链中的父绑定对象检查与该绑定对象上的绑定不匹配的非限定引用,该父绑定对象是对foo已创建对象的调用的绑定对象bar. 由于具有用于结合c,这是使用的绑定使标识符cbar例如,粗略地说:

+−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| 全局绑定对象|
+−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| .... |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
             ^
             | 
             |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| `foo` 调用绑定对象 |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| 一 = 1 |
| b = 2 |
| c = 3 |
| bar = (函数) |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
             ^
             | 
             |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| `bar` 调用绑定对象|
+−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| d = 3 |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−−+

有趣的事实:这个作用域链是 JavaScript 中全局变量的工作方式。注意上面的“全局绑定对象”。因此,在函数中,如果您使用的标识符不存在于该函数调用的绑定对象中,并且不存在于该函数与全局绑定对象之间的任何其他绑定对象中,如果全局绑定对象具有绑定为此,使用全局绑定。瞧,全局变量。(ES2015使这个更有趣的一点通过具有两个层,以将全局绑定对象:通过像老式全局声明使用的层var和函数声明,并且通过等较新的使用的层letconstclass,不同的是,旧的层还在全局对象上创建属性,您可以通过window在浏览器上,但较新的层没有。所以全局let声明不会创建window属性,但全局var声明会。)

实现可以自由使用任何他们想要的机制在幕后作出上述似乎发生。无法直接访问函数调用的绑定对象,并且规范明确指出,如果绑定对象只是一个概念,而不是实现的文字部分,那就完全没问题了。一个简单的实现很可能只是按照规范所说的去做;一个更复杂的可能在不涉及闭包时使用堆栈(为了速度优势),或者可能总是使用堆栈,但在弹出堆栈时“撕掉”闭包所需的绑定对象。在任何特定情况下知道的唯一方法是查看他们的代码。:-)

更多关于闭包、作用域链等的信息,请看这里:

谢谢。终于明白闭包了。
2021-03-15 06:25:23
@TJCrowder,你好,Crowder 先生,我非常喜欢你的作品,想请教你关于 JS 中堆栈和堆的问题。首先,据我所知,在函数中保存原始值的变量保存在堆栈中,堆栈是指调用堆栈?其次,全局变量保存在哪里?它在全局执行上下文的堆栈中吗?
2021-04-03 06:25:23
@Gnuey:fooinfoo但不是 in obj.foo,用obj..
2021-04-09 06:25:23
什么是foofoo那是函数本身的标签吗?哪里obj.foo来的呢?
2021-04-09 06:25:23
什么是不合格的参考?
2021-04-12 06:25:23

不幸的是,答案是:视情况而定。

最近的 javascript 引擎发生了很大的变化,开始比以前更好地优化。过去的答案是:“局部变量存储在堆分配的堆栈帧中,以便闭包工作”。它不再那么简单了。

已经(或曾经在 20 到 30 年前)研究过 Scheme 实现和闭包优化(JavaScript 继承了很多 Scheme 闭包,除了使它变得更加棘手的延续之外)。

我没有准备好纸质链接,但是如果您没有非常高效的垃圾收集器,您也需要使用堆栈。棘手的部分是处理闭包,它需要堆分配变量。为此,使用了不同的策略。结果是一个混合体,其中:

  • 通过内联函数,您可以显着减少分配/解除分配的堆分配帧的数量
  • 一些变量可以安全地放在堆栈上,因为它的时间跨度是有限的(它通常也与内联函数调用有关)
  • 在某些情况下,您知道您可能正在创建闭包,但是您可以等到发生这种情况,然后为其分配堆堆栈帧并从堆栈中复制当前值
  • 有与尾调用相关的优化,您可以在其中更早地进行堆分配,然后在下一个函数调用中重用堆栈帧,但据我所知,这在 javascript 引擎中没有使用

这个领域在几个竞争引擎中变化非常快,所以答案可能仍然是“这取决于”

此外,在该语言的新版本中,我们将看到类似这样的特性letconst这实际上使引擎更容易优化分配决策。特别是不变性非常有帮助,因为您可以从堆栈中自由复制值(例如,然后将其作为闭包对象的一部分),而无需解决来自不同闭包的更改变量的冲突。

原始链接(在作者的主页中)是cs.indiana.edu/~dyb/pubs/3imp.pdf
2021-03-20 06:25:23
就我个人而言,对我影响最大的是计划大师 Kent Dybvig cs.unm.edu/~williams/cs491/three-imp.pdf 的这篇论文,还有一些更专业/详细的论文建立在它之上。此外,我最近看到了很多有趣的东西,描述了当前的 JavaScript 引擎以及团队正在取得的进展,就像这样一个wingolog.org/archives/2011/07/05/v8-a-tale-of-two-compilers但他们平时不要走得太深。
2021-03-23 06:25:23
非常感谢!那么除了在这里发布问题之外,我还能在哪里学习这些东西呢?是通过阅读最先进的引擎(他们的文档甚至源代码),还是通过深入研究论文?我对你提到的优化策略特别感兴趣。我在哪里可以找到有关它们的详细信息?再次感谢!
2021-03-28 06:25:23