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

Why aren't functions passed as arguments mutable like how other objects in JS are (objects, arrays)?

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

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]


以上我的理解:

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

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'

当我们将匿名回调传递给 setTimeout 时,它似乎像我们期望的那样引用了重新分配的函数。

这里到底发生了什么?如果函数作为参数传递,为什么它不像另一个对象(例如数组)那样引用重新分配的值。我的理解是,当 setTimeout 执行时,会创建对该函数的引用副本,但我很困惑为什么当其他对象的引用副本作为参数传递给函数时它不遵循其他对象的规则,并且然后可以变异。

因为你不是在做同样的事情。在您的 arrReassigned 函数中,您将一个函数传递给 setTimeout 并关闭 arr。当该函数 运行s 时,它会看到 arr 的 then-current 值。

但是在您的第一个 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−−−−−−>|         (function)         |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
                  | name: "b"                  |
                  | [[Code]]: console.log("b") |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+

“Ref54612”只是代表,但将对象引用视为告诉 JavaScript 引擎对象在内存中其他地方的位置的数字很有用(无论如何对我而言)。

2。它调用 setTimeout(b, 0);b 的当前值传递给 setTimeout,后者将其放入环境计时器列表中的一条记录中,写着“在 X 时间调用它”。此时,我们在内存中有这样的东西:

                                +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
                                |                                              |
                                v                                              |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               |
b: Ref54612−−−−−−>|         (function)         |                               |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               |
                  | name: "b"                  |                               |
                  | [[Code]]: console.log("b") |                               |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               |
                                                                               |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                              |
Host timer list−−>|            (list)           |                              |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      +−−−−−−−−−−−−−−−−−−−−+  |
                  | 0                           |−−−−−>|    (timer info)    |  |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      +−−−−−−−−−−−−−−−−−−−−+  |
                  | {callback: Ref54612, at: X} |      | callback: Ref54612 |−−+
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      | at: X              |
                                                       +−−−−−−−−−−−−−−−−−−−−+

3。它将 new 函数分配给 b 变量。现在我们有这样的东西:

                                +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
                                |                                              |
                                v                                              |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               |
                  |         (function)         |                               |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               |
                  | name: "b"                  |                               |
                  | [[Code]]: console.log("b") |                               |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                               |
                                                                               |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+                              |
Host timer list−−>|            (list)           |                              |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      +−−−−−−−−−−−−−−−−−−−−+  |
                  | 0                           |−−−−−>|    (timer info)    |  |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      +−−−−−−−−−−−−−−−−−−−−+  |
                  | {callback: Ref54612, at: X} |      | callback: Ref54612 |−−+
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+      | at: X              |
                                                       +−−−−−−−−−−−−−−−−−−−−+
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
b: Ref84965−−−−−−>|         (function)         |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+
                  | name: "b"                  |
                  | [[Code]]: console.log("C") |
                  +−−−−−−−−−−−−−−−−−−−−−−−−−−−−+

请注意如何更改 b 中的值(更改为“Ref84965”)对计时器将要调用的函数(“Ref54612”)没有任何影响。

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

5。稍后,当主机环境发现是时候调用 Ref54612 处的函数时,它会调用 — 记录“b”,因为这就是该函数的作用。

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

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

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

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