27

I've been asked about a question

{
  function foo() {
    console.log('A');
  }
  
  foo();
  
  foo = 1;
  
  function foo() {
    console.log('B');
  }
  
  foo = 2;

  console.log(foo);
}
console.log(foo);

Why the third output is 1 instead of 2?


There should be no block scoped foo being created since there is neither let nor const in that block. But the second foo output is 2 means there is indeed another reference of foo has been created.

What is going on?

P.S. I'm using Chrome Version 89.0.4389.90 (Official Build) (x86_64).

Hao Wu
  • 17,573
  • 6
  • 28
  • 60
  • I've tried all sorts of variations of this and can't make sense of what's going on – coagmano Mar 23 '21 at 03:59
  • 7
    FWIW, Safari gives the expected “B 2 2” here. So, at least specify what environment you get those results in. – deceze Mar 23 '21 at 06:30
  • @deceze I didn't check with safari, maybe the V8 is different from JSC while handling block scopes? Which behaviour is correct? – Hao Wu Mar 23 '21 at 06:32
  • 2
    I would *expect* “B 2 2” here, but what is “correct” I can’t say. This behavior is probably “correct” according to some definition. The question is: why the difference? – deceze Mar 23 '21 at 06:36
  • Have you tried to debug it? Node.js gives the same output. I used my debugger and I can see one `foo` in `Block` and one `foo` in `Local` with different value. Before `foo = 1;` both variables contain functions. `foo = 1;` sets both variables to `1`. But `foo = 2;` only sets the `Block` variable to `2`. `console.log(foo);` inside the block prints the block variable and `console.log(foo);` at the end prints the `Local` variable (there is no more `Block`). –  Mar 23 '21 at 10:03
  • 1
    @trincot probably because the top answer to that breaks down exactly what's happening at the block level to explain why this behaviour occurs? The presence or absense of ES6 syntax doesn't seem to matter – coagmano Mar 23 '21 at 10:09
  • 1
    If you remove the functions from the code only one variable in `Gloabl` is set with `foo = 1;`. No variables in `Local` or `Block`. This means `function foo()` creates variables in `Local` and `Block`. `foo = 1;` sets variables in `Local` and `Block` if they already exist (and contain functions) or creates a variable in `Global`. `foo = 2;` sets a variable in `Block` because the variable in `Local` contains a number. –  Mar 23 '21 at 10:13
  • It looks like though the function declaration is hoisted the actual position is important and changes the behavior. You can remove the function call. See [this](https://jsfiddle.net/roupnetv/) and [this](https://jsfiddle.net/6vp7ckn1/1/) and [this](https://jsfiddle.net/r75g8ybn/) –  Mar 23 '21 at 11:08
  • 2
    @trincot negatory on the lack of ES6. Block scope *does not exist* before ES6. Moreover, there weren't block scoped functions before ES6 (given that there was no block scope, not surprising, but goes beyond that with `if (true) function f() { return true; } else function f() { return false;}` creating only the latter function. Post-ES6 this is not *corrected*, so only the function declaration in the branch that runs is defined, rather than the last one always overwriting the previous. This is *very much* an ES6 specific question. – VLAZ Mar 23 '21 at 12:21
  • @VLAZ, agreed that this behaviour is ES6 related. But [this Q&A](https://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6) deals only with function scoping. Although it has relevant information, it does not really address the question here, in my opinion. – trincot Mar 23 '21 at 12:56
  • 1
    @deceze then Safari is wrong. – Jonas Wilms Mar 23 '21 at 18:38
  • Wait, actually I think I know what's happening, you were not actually allowed to define functions in block scopes. – Benjamin Gruenbaum Mar 23 '21 at 18:42
  • https://stackoverflow.com/questions/25111087/why-is-a-function-declaration-within-a-condition-block-hoisted-to-function-scope probably – Benjamin Gruenbaum Mar 23 '21 at 18:44

2 Answers2

7

According to the web compat semantics at the place of the function declaration, the value of the blocked scope variable is bound to the outer scope². This code is equivalent to:

let outerFoo; // the functions create a binding outside of the scope

{
  let innerFoo; // but also inside
  // due to hoisting, functions get bound before any code get's executed:
  innerFoo = function foo() {
    console.log('A');
  };
  innerFoo =   function foo() {
    console.log('B');
  };
  
  // At the place of the function declaration, the variable leaves the scope
  /* function foo() {
    console.log('A');
  } */
  outerFoo = innerFoo;

  innerFoo();
  
  innerFoo = 1;
  
  // this also applies to the second declaration
  /* function foo() {
    console.log('B');
  } */
  outerFoo = innerFoo;
  
  innerFoo = 2;

  console.log(innerFoo);
}
console.log(outerFoo);

²This is basically exactly how the specification describes it:

When the FunctionDeclaration f is evaluated, perform the following steps in place of the FunctionDeclaration Evaluation algorithm provided in 15.2.6:
a. Let fenv be the running execution context's VariableEnvironment.
b. Let benv be the running execution context's LexicalEnvironment.
c. Let fobj be ! benv.GetBindingValue(F, false).
d. Perform ! fenv.SetMutableBinding(F, fobj, false).

The specification additionally states:

Prior to ECMAScript 2015, the ECMAScript specification did not define the occurrence of a FunctionDeclaration as an element of a Block statement's StatementList. However, support for that form of FunctionDeclaration was an allowable extension and most browser-hosted ECMAScript implementations permitted them. Unfortunately, the semantics of such declarations differ among those implementations. Because of these semantic differences, existing web ECMAScript code that uses Block level function declarations is only portable among browser implementation if the usage only depends upon the semantic intersection of all of the browser implementations for such declarations

So Safari is probably doing it the way it always did it, while Chrome (and Firefox) follow the specification.

Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • 2
    @BenjaminGruenbaum yeah, WebCompat semantics are my favourite part of the specification, because the behavior seems so random if you don't know how it is specified :) Already answered a few of these questions so I have this one basically bookmarked – Jonas Wilms Mar 23 '21 at 18:51
  • 1
    Just imagine these things (like `__proto__`) are still being actively debated in TC39 meetings to this day - the last change here was in November IIRC and it's still in agendas https://github.com/tc39/ecma262/pull/2125 . – Benjamin Gruenbaum Mar 23 '21 at 18:54
2

This is an analysis of what's happening with a debugger in Node.js. It doesn't explain why this is happening.

There are 3 scopes involved: local scope, global scope and block scope.

I did the same analysis in a Chrome browser. The behavior was similar with the only difference that there is no local scope and instead of the local scope the global scope was used.

The code

{
    foo = 1;
    foo = 2;
    console.log(foo);
}
console.log(foo);

creates a variable in global scope and sets two different values for that variable. In this code

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

the line function foo() { } creates a variable foo in block scope and a variable foo in local scope (global scope in Chrome). Since there exists a variable in block scope foo = 1; sets a value for the existing variable in block scope and doesn't create a variable in global scope. foo = 2; sets a different value for the same variable. The first console.log(foo); prints 2 from block scope and the second console.log(foo); prints f foo() { } from local scope (global scope in Chrome).

In this code

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

the function declaration function foo() { } is hoisted and creates a variable foo in block scope with value f foo() {} and a variable foo in local scope (global scope in Chrome) with value undefined. The line foo = 1; sets both variables to 1. The line foo = 2; sets the variable in block scope to 2. The first console.log(foo); prints 2 from block scope and the second console.log(foo); prints 1 from local scope (global scope in Chrome).

In this code

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

the function declaration function foo() { } creates a variable foo in block scope with value f foo() {} and a variable foo in local scope (global scope in Chrome) with value f foo() {}. The line foo = 1; sets both variables to 1. The line foo = 2; sets the variable in block scope to 2. The first console.log(foo); prints 2 from block scope and the second console.log(foo); prints 1 from local scope (global scope in Chrome).

  • 1
    What does "local scope" mean? Is that function scope when the code is inside a function definition? That's the normal scoping of traditional `var` and `function` dclarations. – Barmar Mar 23 '21 at 17:27
  • @Barmar I don't know what local scope means. It's what the debugger shows me. Here is a screenshot: https://imgur.com/a/jsUXMVq You can see a variable `foo` in `Block` and one variable `foo` in `Local`. That's two different variables. There is also a different Global section. –  Mar 23 '21 at 17:57