Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scoping and closure oddities in javascript

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.

like image 475
stratis Avatar asked Oct 30 '22 16:10

stratis


1 Answers

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).

like image 151
T.J. Crowder Avatar answered Nov 09 '22 15:11

T.J. Crowder