1

Can anyone make sense of the difference in behaviour between the two snippets described below?

Snippet #1

{
    function f() {return 1}
    f = function() {return 2}
    function f() {return 3}
}
console.log(f()); // yields 2

Snippet #2

{
    function f() {return 1}
    f = function() {return 2}
}
console.log(f()); // yields 1

This first snippet yields the result I would expect, based on my understanding of what the interpreter does:

  1. During the declaration phase, the interpreter enters the block.
  2. It identifies the first function declaration of f. Hoists the declaration to the module scope and hoists the initialization to the top of the block scope.
  3. It then sees an assignment f = function() {return 2}, which it ignores, since it is still in declaration phase.
  4. Moves on to the second declaration function f() {return 3}, repeating the process in 2., which effectively replaces the initialization of f.
  5. During the evaluation phase, declarations are ignored. The interpeter enters the block and sees the assignment f = function() {return 2}, which sets the value of f across the whole module scope.
  6. It exits the block and prints f(), which correctly yields 2.

The second snippet yields a bizarre result. I expected to still get 2 as a printed result, yet I get 1. The interpreter should do the same as before. During the evaluation, the last value of f should be f = function() {return 2}, as before. Any ideas?

Thanks in advance for any insights into this.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
pdlaf
  • 21
  • 2
  • 1
    It gets even weirder when you add `use strict`. The second example will throw an error that `f` has already been declared but in the `console.log(f())` line. Commenting out `console.log(f())` removes the error – Konrad Nov 17 '22 at 21:16
  • @KonradLinkowski Thanks. Yes, but that actually makes sense, according to my understanding of the interpreter. It's my second snippet that yields an unexpected result. – pdlaf Nov 19 '22 at 20:17

1 Answers1

1

According to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function the behaviour of the function syntax in blocks is undefined or at least inconsistently implemented across browsers.

The block seems to affect where the function definition gets hoisted to. For example:

f = function() {return 2}
{
    function f() {return 1}
}
console.log(f());

Outputs 1 because the later definition only seems to get hoisted to the top of its block. Whereas:

f = function() {return 2}
function f() {return 1}
console.log(f());

Outputs 2 because the second definition gets hoisted to the top, and therefore is the one that gets overwritten.

However, in strict mode the functions created by the statements are locally scoped, like you'd probably expect. So:

"use strict";
{
    function f() {return 1}
}
console.log(f());

Throws a "f is not defined" error. While:

"use strict"
let f = function() {return 2} // The let is needed due to the strict mode
{
    function f() {return 1}
}
console.log(f());

Now outputs 2 instead of 1, as the second function is a separate variable which now shadows the first function during the block, as opposed to changing it.

And lastly, I'm still not entirely sure what's happening with that last one. While:

{
    function f() {return 1}
    f = function() {return 2}
}
console.log(f());

Outputs 1 like you said, removing the block causes it to output 2 like you'd normally expect. And that's also the case if the block is only around the second definition.

However, I did find that using a function statement inside a block seems to create 2 variables, both with the same value, but one globally scoped and one locally scoped. This seems to be completely different to how variable assignments normally work, even when you use the older var keyword. So these all just either create a single local or global variable, but each will never produce both on its own:

{
    // You'd only run one of these at a time by commenting all but 1 out
    let a = 1; // Block
    var a = 1; // Global
    a = 1; // Global
    this.a = 1; // Global
    debugger;
}

And because there's a block scoped variable with the same name, it shadows the global inside the block. And so the second definition sets the block scoped variable instead of the global one. Which means that your second example doesn't work as expected.

The 1st only seems to work because the 3rd function definition seems to set this.f to match the block scoped f:

{
    function f() {return 1}
    f = function() {return 2}
    debugger;
    // ^ this.f is the last function, while the block scoped f is the second function

    function f() {return 3}
    // ^ This seems to make this.f match the block scoped f.
    // Even though it shouldn't do anything here because it was hoisted up

    debugger; // The values stay the same
}
debugger; // The values stay the same, but block scope is deleted
console.log(f()); // 2

So the somewhat satisfactory answer is that your second definitions in your snippets are block scoped, as opposed to globally scoped. On the other hand, the first definitions are simultaneously globally and block scoped (it makes 2 variables with the same value). The block scope value is the only one that gets directly overwritten by the second definition. And the reason the first snippet works is because apparently not all of the 3rd function gets hoisted. So after the second function definition, the 3rd function definition sets the value of the globally scoped function to the value of the block scoped function - for some reason. So that then means the second function gets called because that's the one in the global scope.

Well, that was interesting researching. Hope that helps. I'm assuming you want to know out of curiosity right? Because if you're relying on this behaviour, there's almost certainly a better way.

Nico
  • 83
  • 1
  • 7
  • Hey @Nico, thanks for your thorough reply, that was great. I have to look at it in detail. But since Javascript occasionally shows such unexpected results, I think I also need to go back to the language specification. I know some block behavior is either undefined or inconsistent, but I'm hoping most of what I need might be in the specification. To answer your question, it's not curiosity. I'm building a program analysis tool (creating diagrams from code), so I need to cover all cases (or as close as possible). I thought I had this behavior down until I came across this particular example. – pdlaf Nov 19 '22 at 21:24
  • Ah ok. I just did a little experimentation and skimmed some mdn docs so there's probably a bit more detail you can find in the specs. But couldn't you just require strict mode is used and avoid having to deal with this weirdness as well as some other situations? – Nico Nov 19 '22 at 21:51
  • Trying to implement as much of this weirdness as possible also seems kind of cool though. Although I imagine you might end up reading the entire spec which might take a while :P – Nico Nov 19 '22 at 21:55
  • Yeah, I guess I could start there. But when I started out I thought it would be more straightforward, so was trying to cover things in 'non-strict' mode. But if it turns out to be too hard I might change gears and focus on 'strict'. Thanks for reminding me!:) As for the specs, I was actually just now thinking of how to approach it, since it's huge. Maybe I can focus on a small set of details that I need for now. Still, it'll take me a good 3 to 5 days. Once I have this down the rest should be quick (2 weeks). The output should be the whole function call graph of a codebase. – pdlaf Nov 19 '22 at 23:34