简体   繁体   中英

Javascript in Node.js shows bizarre behaviour involving functions and block scopes

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM