为什么函数不像JS中的其他对象(对象,数组)那样作为参数可变传递?



例如,我们知道,如果对数组的引用在函数范围内发生突变或重新赋值,则指向该数据的任何指针也将指向突变/重新赋值。

function arrReassigned() {
let arr = [1,2,3];
setTimeout(() => console.log(arr), 0);
arr = [4,5,6];
}
function arrMutates() {
const arr = [1,2,3];
setTimeout(() => console.log(arr), 0);
arr.push(4);
}
let nums = [1,2,3];
function mutateNums(nums) {
nums.push(4);
}
arrReassigned(); // [4,5,6]
arrMutates(); // [1,2,3,4]
mutateNums(nums);
console.log(nums); // [1,2,3,4]

我对上述内容的理解:

  • 最初分配arr
  • setTimeout 被推送并立即从调用堆栈中弹出,回调 arg 在 ~0 毫秒后被推送到 JS 任务队列
  • 对数组的引用被重新分配,或者它指向的值被改变
  • 调用堆栈
  • 为空,因此事件循环将回调从任务队列推送到调用堆栈。回调按预期执行并引用重新分配/更改的 arr 变量。

我试图通过函数重新分配来重新创建它,但它的行为不符合预期。

function a() {
function b() { console.log('b') }
setTimeout(b, 0);
b = function() { console.log('c') }
b();
}
a(); // logs 'c' then 'b'

当函数b作为参数传入并最终通过任务队列调用时,它不会引用重新分配的值。

function a() {
function b() { console.log('b') }
setTimeout(() => b(), 0);
b = function() { console.log('c') }
b();
}
a(); // logs 'c' then 'c'

当我们将一个 anon 回调传递到 setTimeout 中时,它似乎像我们期望的那样引用了重新分配的函数。

这到底是怎么回事?如果函数作为参数传递,为什么它不像另一个对象(例如数组)那样引用重新分配的值。 我的理解是,在 setTimeout 执行时会创建对函数的引用的副本,但我对为什么当它们的引用副本作为参数传递到函数中然后可以改变时,为什么它不遵循其他对象的规则感到困惑。

重要的是何时查找变量以获取其值。如果在回调函数中查找变量,它将在重新赋值后获得其最新值。

当您执行setTimeout(b, 0)时,b会在您呼叫setTimeout()时查找。这将获取原始函数对象,并将其传递给setTimeout()。重新赋值变量对此没有影响。

当你执行setTimeout(() => b(), 0)时,你正在将匿名函数传递给setTimeout()。调用该函数时,它会查找b并获取其更新的值。

因为你没有做同样的事情。在arrReassigned函数中,您将一个函数传递到setTimeout中,该函数在arr上关闭。当该函数运行时,它会看到当时的当前值arr

但是在你的第一个a函数中,你b本身传递到setTimeout。然后你给b分配一个新值,但这对你传递给setTimeout的值没有任何影响。

如果通过传入一个在b上关闭的函数使aarrReassigned相同,则会看到预期的结果:

function a() {
function b() { console.log('b') }
setTimeout(() => b(), 0); // ***
b = function() { console.log('c') }
b();
}
a(); // logs 'c' then 'c'

在这方面,函数对象和数组对象(或任何其他类型的对象)的处理方式没有区别。例如,事情是相反的,使arrReassigned看不到重新分配的原因与你的第一个a没有看到的原因相同:

function example(a) {
setTimeout(() => console.log(a), 0);
}
function arrReassigned() {
let arr = [1,2,3];
example(arr);
arr = [4,5,6];
}
arrReassigned(); // Logs [1, 2, 3]

这就像你的第一个a函数:它将对象引用传递到example,然后更改来自的变量,这对之前传入的内容没有影响,所以example日志[1, 2, 3]


您似乎特别有疑问的部分是(第一个a):

function a() {
function b() { console.log('b') }
setTimeout(b, 0);
b = function() { console.log('c') }
b();
}
a(); // logs 'c' then 'b'

让我们来看看当您调用a() 时会发生什么:

1. 它创建一个函数并将对该函数的引用分配给局部变量b。此时,我们在内存中有类似的东西:

+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
b: Ref54612−−−−−−−>|        (功能) | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |名称:"B" | |[[代码]]: 控制台.log("b") | +
−−−−−−−−−−−−−−−−−−−−−−−−−−−+">

Ref54612"只是代表性的,但将对象引用视为告诉JavaScript引擎对象在内存中其他位置的数字是很有用的(无论如何,对我来说)。

2.它调用setTimeout(b, 0);这会将b的当前值传递到setTimeout,这会将其放在环境的计时器列表中的记录中,上面写着"在 X 时间调用它"。此时,我们在内存中有类似的东西:

+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
|                                             | V | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               | b: Ref54612−−−−−−−>|        (功能) |                              | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               | |名称:"B" |                              | |[[代码]]: 控制台.log("b") |                              | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               |  | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                              |主机定时器列表−−>|           (列表) |                             | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      +−−−−−−−−−−−−−−−−−−−−+  | |0 |−−−−−>|   (计时器信息) | | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      +−−−−−−−−−−−−−−−−−−−−+  | |{回调: Ref54612, 在: X} |     |回调: ref54612 |−−+ +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      |在: X |  +−−

−−−−−−−−−−−−−−−−−−−−−+3.它将一个新函数分配给b变量。现在我们有这样的东西:

+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
|                                             | V | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               | |        (功能) |                              | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               | |名称:"B" |                              | |[[代码]]: 控制台.log("b") |                              | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               |  | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                              | 主机定时器列表−−>|           (列表) |                             | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      +−−−−−−−−−−−−−−−−−−−−+  | |0 |−−−−−>|   (计时器信息) | | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      +−−−−−−−−−−−−−−−−−−−−+  | |{回调: Ref54612, 在: X} |     |回调: ref54612 |−−+ +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      |在: X |  +−−−−−−−−−−−−−−−−−−−−+ +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ b: Ref84965−−−−−−>|        (功能) | +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |名称:"B" | |[[代码]]: 控制台.log("C") | +

−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−b

−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−4.它调用b(),它记录c,因为这是新函数的作用。

5. 稍后,当主机环境看到是时候在 Ref54612 调用该函数时,它会记录"b",因为这就是该函数的作用。

相比之下,在arrReassigned示例中,您将一个函数(不是arr)传递给setTimeout(因为将arr传递给它没有意义),后来当函数运行时,它会查找arr的值,并使用您分配给它的新数组。

最新更新