5

This was presented yesterday at TC39. You can find the gist here:

var p = () => console.log(f);
{
  p(); // undefined
  console.log(f); // function f(){}

  f = 1;

  p(); // undefined
  console.log(f); // 1

  function f(){}

  p(); // 1
  console.log(f); // 1

  f = 2;

  p(); // 1
  console.log(f); // 2
}

Could someone please explain to me how this thing works? For the record it's only working in non-strict mode.

Thank you.

stratis
  • 7,750
  • 13
  • 53
  • 94
  • 1
    Specifically in relation to the hosting? This answer explains it very well: http://stackoverflow.com/questions/25111087/why-is-a-function-declaration-within-a-condition-block-hoisted-to-function-scope – CodingIntrigue Nov 30 '16 at 13:13
  • 1
    @CodingIntrigue: No, that doesn't cover what's happening above, which is an artifact of ES2015's Annex B and TC39 trying their best to steer through seriously treacherous waters of existing code and historically-inconsistent implementations. :-) – T.J. Crowder Nov 30 '16 at 13:15
  • @T.J.Crowder Care to explain in more detail? `function f(){}` would normally be hoisted inside a normal function block and the first `undefined` should have printed the function f declaration but what about now that's defined inside a block? – stratis Nov 30 '16 at 13:18
  • @kstratis: I'm trying to, but I want to be very careful not to give you incorrect information. FYI, the relevant bit is [B.3.3.1](http://www.ecma-international.org/ecma-262/7.0/index.html#sec-web-compat-functiondeclarationinstantiation), in particular the jiggery-pokery in step 1.a.ii.3. But I haven't been through that section in detail yet (and have to step out, sadly) so I don't have a *ready* explanation to hand. *Fascinating* situation. – T.J. Crowder Nov 30 '16 at 13:20
  • I'm not sure but this is probably related to the different scope of arrow functions in es6 have compared to "normal" js behaviour. – Sven van de Scheur Nov 30 '16 at 13:22
  • @SvenvandeScheur: No, definitely not that. You'd get exactly the same output if the first line were `var p = function() { console.log(f); };` – T.J. Crowder Nov 30 '16 at 13:23
  • 1
    @T.J.Crowder Thanks for the link, seems like the issue is more complicated than I realised. – CodingIntrigue Nov 30 '16 at 13:27

1 Answers1

2

I will not claim to understand all the subtleties, but the key thing about this the almost bizarre contortions of Annex B's §B.3.3.1.

That code is effectively this, where f1 is a second copy of f specific to the lexical environment of the block (hence let below):

var p = () => console.log(f);
{
  let f1 = function f(){};;           // Note hoisting
  p(); // undefined
  console.log(f1); // function f(){}

  f1 = 1;

  p(); // undefined
  console.log(f1); // 1

  var f = f1;                          // !!!

  p(); // 1
  console.log(f1); // 1

  f1 = 2;

  p(); // 1
  console.log(f1); // 2
}

And of course, thanks to var hoisting, both p and f are effectively declared at the top of the code snippet with the initial value undefined:

var f = undefined;
var p = undefined;
p = () => console.log(f);
{
  let f1 = function f(){};;           // Note hoisting
  p(); // undefined
  console.log(f1); // function f(){}

  f1 = 1;

  p(); // undefined
  console.log(f1); // 1

  f = f1;                              // !!!

  p(); // 1
  console.log(f1); // 1

  f1 = 2;

  p(); // 1
  console.log(f1); // 2
}

The key bit from B.3.3.1 is that it transfers the value of the inner f (which I've called f1 above) to the outer one (in the below, F is the string "f", the name of the function being declared):

3. When the FunctionDeclaration f is evaluated, perform the following steps in place of the FunctionDeclaration Evaluation algorithm provided in 14.1.21:

a. Let fenv be the running execution context's VariableEnvironment.

b. Let fenvRec be fenv's EnvironmentRecord.

c. Let benv be the running execution context's LexicalEnvironment.

d. Let benvRec be benv's EnvironmentRecord.

e. Let fobj be ! benvRec.GetBindingValue(F, false).

f. Perform ! fenvRec.SetMutableBinding(F, fobj, false).

g. Return NormalCompletion(empty).

Recall that the variable environment is function-wide, but the lexical environment is more constrained (to the block).

When it comes to trying to normalize function declarations in places where they were {invalid | unspecified} (choose your term), TC39 have a very treacherous path to navigate, trying to standardize behavior while not breaking existing code that may have relied on implemenation-specific behaviors from the past (which were mutually-exclusive, but TC39 is trying to strike a balance).

Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Could you possible elaborate a bit more on the first two `p()`? Why do they come out as `undefined`? That bit troubles me the most; We already have hoisting and thus `f` should be already defined at that point... – stratis Nov 30 '16 at 14:09
  • 1
    @kstratis remember that hoisting doesn't _set_ a variable. All it does is change the declaration, so if you have, say `var x = 5` on line 10, _in effect_ the code is `var x;` at the top of the file, and then `x = 5` on line 10. Lines 1-9 would still have `x` equal `undefined` since the value won't be set yet. So doing `console.log(x)` on line 4 would produce `undefined`. However, if you _didn't_ have `var x` at all, it would produce ReferenceError – VLAZ Nov 30 '16 at 14:12
  • 1
    @kstratis: See vlaz's comment above. I've also added a second code snippet to make that explicit. – T.J. Crowder Nov 30 '16 at 14:54
  • @T.J.Crowder Thanks for helping me get to the bottom of this. One last piece of this puzzle is still frying my brain though: Half way through the snippet we get this: `function f(){}; p() //1`. How is this possible and how do you translate this to `f=f1; p() //1`? Thanks. – stratis Dec 01 '16 at 12:01
  • 1
    @kstratis: That's the bit I quote from the spec at the end, where it's transferring the value of the inner `f` (which I've called `f1`) to the outer `f`. It's truly bizarre, but again, they're trying to steer the best course to avoid making too much code in the wild break. – T.J. Crowder Dec 01 '16 at 12:10
  • @T.J.Crowder Right. Could you please quote for me the exact bit (down to the line) from the spec which dictates `f=f1;` I simply can't read the spec. Too many unknowns. – stratis Dec 01 '16 at 12:36
  • 1
    @kstratis: The quoted bit *is* that bit, they just really draw it out. :-) (a) and (b) are basically saying "get the overall function's variable environment" and (c) and (d) are saying "get the block's lexical environment", then (e) reads the value of the inner `f` (`f1`) from the block's environment, and (f) writes that value to the outer `f` in the variable environment. (And yes, the spec is clear as mud.) – T.J. Crowder Dec 01 '16 at 12:43
  • @T.J.Crowder Thank you. I think I now get it. Your help is much appreciated. – stratis Dec 01 '16 at 12:46
  • @kstratis: No worries -- it was a fascinating one! And the spec is amazingly, amazingly hard to read... :-) – T.J. Crowder Dec 01 '16 at 12:46