Javascript 参数传递如何与嵌套函数调用上的调用堆栈一起工作
假设您有这些调用(JavaScript,只是为了弥补本文的不足): 因此,为了不深入讨论编译器对此的精确表示,我主要感兴趣的是演示一些嵌套变量,以及它们如何在函数调用之间使用 基本上,我们在每个“框架”(我只是指在即将到来的函数调用之前定义的所有变量)上都有: 因此,在顶层,Javascript 参数传递如何与嵌套函数调用上的调用堆栈一起工作,javascript,function,assembly,process,operating-system,Javascript,Function,Assembly,Process,Operating System,假设您有这些调用(JavaScript,只是为了弥补本文的不足): 因此,为了不深入讨论编译器对此的精确表示,我主要感兴趣的是演示一些嵌套变量,以及它们如何在函数调用之间使用 基本上,我们在每个“框架”(我只是指在即将到来的函数调用之前定义的所有变量)上都有: 因此,在顶层,x和b都会更改它们的值。否则,所有其他变量只声明一次。但实际上,函数调用可能有20个深度,每个深度有20个变量,其中一些被赋值10次+。因此,如果不是更多的话,这种情况将是10倍 基本上,我想知道调用堆栈在每个帧中是什么样子
x
和b
都会更改它们的值。否则,所有其他变量只声明一次。但实际上,函数调用可能有20个深度,每个深度有20个变量,其中一些被赋值10次+。因此,如果不是更多的话,这种情况将是10倍
基本上,我想知道调用堆栈在每个帧中是什么样子的。特别是在某些点之后如何存储/恢复局部变量
例如,第(2)点和第(3)点之间发生了什么?x
和b
变量都被重新定义。在(2)处调用函数之前,调用堆栈中有什么内容?(2)中的调用堆栈中包含哪些内容?在(2)之后,什么进入调用堆栈
假设我们有一个更复杂的情况:
let a = 10
let b = 20
draw(a++, b)
draw(a++, b)
draw(a++, b)
draw(a++, b)
...x100
然后呢?b
是否每次调用draw
时都会被推送和弹出?或者是否以某种方式对其进行了优化,使其不必每次都存储在堆栈上?这种东西
在很大程度上,我只是想了解如何从头开始构建一个调用堆栈,但对于实际将什么放入调用堆栈,以及函数返回时实际弹出的时间/方式/内容,我感到困惑。因为对我来说,在我的头脑中,当你做让b=10
,它只是“停留在函数范围内”,直到函数完成,但这是不现实的。我没有注意调用堆栈,主要是因为高级语言不需要您,所以我对它没有任何感觉。我想获得的是一种直觉,了解调用堆栈在这些点/帧上的外观。我看过wikipedia(和其他)绘制堆栈矩形的图表,但它们不是很有帮助。我认为真正有用的是一些伪代码,这样我就可以看到,也许在一组“框架”中,每个“步骤”或“框架”在JavaScript对象或结构(类似的东西)方面是什么样子的,比如:
var callStackAtEachFrame = [
{
a: 10
},
[
{
a: 10
},
{
a: x * 2
}
],
[
{
a: 10
},
{
a: x * 2,
b: doZ(x) + 2
},
{
something: Math.pow(2, x)
}
]
]
我真的不知道,但似乎以这样一种更为代码化的方式,可视化事物如何从调用堆栈中推送和弹出,将有助于了解如何构建调用堆栈
在现实中看起来像这样吗
function start() {
let a = 10
PUSH(a)
let x = doX(a)
POP(a, x)
let b = 20
PUSH(a, x, b)
doY(a, b + x)
POP(a, x, b)
b = 30
x--
PUSH(a, x, b)
return doZ(a + (b * x))
}
function doX(x) {
let a = x * 2
PUSH(a)
let b = doZ(x) + 2
POP(a, b)
return doZ(a + b)
}
...
另外,我并不真正感兴趣JavaScript本身是如何工作的,我感兴趣的是一种与语言无关的方式。首先,在大多数语言中,局部变量(模闭包)不能在声明它们的函数范围之外修改或访问,因此,另一个函数中的变量是完全独立的,即使它们的名称与另一个函数中的变量名称相同。我之所以这样说,是因为您的示例在不同的函数中重用了变量名,我认为这会留下一些混淆的空间 正如您所注意到的,堆栈不仅仅用于函数调用和参数传递,它还存储局部变量。然而,虽然函数调用参数和返回地址有时会被推送到堆栈上,但局部变量通常是在组中分配的,而不是被推送到堆栈上 大多数语言实现在函数的顶部创建一次函数所需的所有本地存储(对于局部变量和临时变量) 实际上,尽管局部变量可能在同一个函数中的不同范围内,但您可能设想的所有局部变量的推送和弹出操作都被展平并合并到单个组分配中
fn(x) {
int a = ...;
if (...) {
int b = ...;
}
while (...) {
int c = ...;
}
}
这里假设a
、b
和c
需要本地存储,语言实现将在序言中分配最多3 int的本地存储,并在结尾中取消分配。(例如,如果该语言意识到b
和c
的活跃度/存储持续时间不重叠,则它们可以共享相同的存储位置,则分配的资源可能会更少。)
在序言中,我描述的组分配通常是通过从堆栈指针中减去值来完成的,而不是将值推送到堆栈上。存储块本质上是未初始化的(尽管各种语言实现都有不同的技术来确保变量在执行期间正确初始化)
形式参数类似于局部变量,但它们是由某些调用方使用值初始化的,而局部变量是在使用它们的同一个函数中初始化的
fn(x) {
a = fn2(x+1,x-1);
... a ..;
}
fn2(y,z) { return y * z; }
在上面,理论上,x+1
和x-1
是在函数fn
的上下文中进行评估的。当实际调用fn2
时,我们将看到y:=x+1
和z:=x-1
,这发生在fn2
的第一行实际运行之前。形式参数y
和z
的初始化实际上发生在实际调用fn2
之前,因此,在它们作为形式参数存在于被调用函数fn2
中之前,必须使用一些机制来允许这种初始化
对于基于堆栈的参数传递机,这种机制是push
每个push分配一个变量,该变量将从实际参数的值转换为被调用方的形式参数
要在基于堆栈的机器上完成上述调用,我们将计算x-1
,然后将其推送到堆栈上;接下来计算x+1
并将其推到
fn(x) {
int a = ...;
if (...) {
int b = ...;
}
while (...) {
int c = ...;
}
}
fn(x) {
a = fn2(x+1,x-1);
... a ..;
}
fn2(y,z) { return y * z; }