Home > front end >  Why aren't functions passed as arguments mutable like how other objects in JS are (objects, arr
Why aren't functions passed as arguments mutable like how other objects in JS are (objects, arr

Time:05-01

For example, we know that if a reference to an array gets mutated or reassigned within the scope of a function, any pointers to that data will also point to the mutated/reassigned value.

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]


My understanding of the above:

  • arr is initially assigned
  • setTimeout is pushed and immediately popped off call stack, callback arg gets pushed into JS task queue after ~0 milliseconds
  • The reference to the array gets reassigned or the value that it points to gets mutated
  • Call stack is empty, so event loop pushes the callback from the task queue onto the call stack. Callback executes and references the reassigned/mutated arr variable as expected.

I tried to recreate this with function reassignment, but it doesn't behave as expected.

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

a(); // logs 'c' then 'b'

When function b is passed in as an argument and eventually called via the task queue, it doesn't reference the reassigned value.

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

a(); // logs 'c' then 'c'

When we pass an anon callback into setTimeout instead, it seems to reference the reassigned function as we would expect.

What exactly is going on here? If the function is passed as an argument, why does it not reference the reassigned value like another object would (e.g. arrays). My understanding is that a copy of the reference to the function is created when setTimeout executes, but I'm confused as to why it doesn't follow the rules of other objects when a copy of their reference is passed as arguments into a function and can then be mutated.

CodePudding user response:

What matters is when the variable is looked up to get its value. If you look up the variable in the callback function, it will get its latest value, after the reassignment.

When you do setTimeout(b, 0), b is looked up at the time you call setTimeout(). That gets the original function object, and passes that to setTimeout(). Reassigning the variable has no effect on this.

When you do setTimeout(() => b(), 0), you're passing the anonymous function to setTimeout(). When that function is called, it looks up b and gets its updated value.

CodePudding user response:

Because you're not doing the same thing. In your arrReassigned function, you're passing a function into setTimeout that closes over arr. When that function runs, it sees the then-current value of arr.

But in your first a function, you're passing b itself into setTimeout. Then you assign b a new value, but that has no effect whatsoever on the value you passed into setTimeout.

If you make a the same as arrReassigned by passing in a function that closes over b, you see the result you expect:

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

a(); // logs 'c' then 'c'

There is no difference in how function objects and array objects (or any other kind of object) are handled in this regard. For instance, here's things going the other way, making arrReassigned not see the reassignment for the same reason your first a didn't:

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]

That's exactly like your first a function: It passes the object reference into example, then changes the variable that came from, which has no effect on what was passed in earlier, so example logs [1, 2, 3].


The part you seem particularly to have questions about is this (the first a):

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

a(); // logs 'c' then 'b'

Let's walk through what happens when you call a():

1. It creates a function and assigns a reference to that function to the local variable b. At this point, we have something like this in memory:

                   −−−−−−−−−−−−−−−−−−−−−−−−−−−− 
b: Ref54612−−−−−−>|         (function)         |
                   −−−−−−−−−−−−−−−−−−−−−−−−−−−− 
                  | name: "b"                  |
                  | [[Code]]: console.log("b") |
                   −−−−−−−−−−−−−−−−−−−−−−−−−−−− 

The "Ref54612" is just representative, but it's useful (to me, anyway) to think of object references as numbers that tell the JavaScript engine where the object is elsewhere in memory.

2. It calls setTimeout(b, 0); That passes the current value of b into setTimeout, which puts that in a record in the timers list of the environment saying "call this at X time". At this point, we have something like this in memory:

                                 −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− 
                                |                                              |
                                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. It assigns a new function to the b variable. Now we have something like this:

                                 −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− 
                                |                                              |
                                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") |
                   −−−−−−−−−−−−−−−−−−−−−−−−−−−− 

Notice how changing the value inside b (to "Ref84965") had no effect whatsoever on what function the timer is going to call ("Ref54612").

4. It calls b(), which logs c since that's what the new function does.

5. Later, when the host environment sees that it's time to call the function at Ref54612, it does — logging "b", since that's what that function does.

In contrasts, with your arrReassigned example, you passed a function (not arr) to setTimeout (since passing arr to it wouldn't make sense), and later when the function was run it looked up the value of arr then, and used the new array you'd assigned to it.

  • Related