Search code examples
javascriptlanguage-lawyerstandards

ReferenceError: can't access lexical declaration 'Foo' before initialization


Running the following code:

<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
<script>
  main()
  class Foo { }
  function main() { Foo() }
</script>
</body>
</html>

generates ReferenceError: can't access lexical declaration 'Foo' before initialization (on Firefox 91.5.1esr, anyway).

A simple fix is to move the class definition up a line. Replacing class Foo with function Foo() will also eliminate the error message. Changing Foo() to new Foo(), does not help: the error remains.

Question: Is there a reference to the standards, and/or a clear description, explaining this behavior?


Solution

  • A class is a ClassDeclaration in the spec. This is classified as a DeclarationPart, which is classified as a StatementListItem for the purposes of LexicallyScopedDeclarations.

    When a block or function is evaluated initially (before the code in the block actually starts running), it will do something like:

    33. Let lexDeclarations be the LexicallyScopedDeclarations of code.
    34. For each element d of lexDeclarations, do
      a. NOTE: A lexically declared name cannot be the same as a function/generator declaration, formal parameter, or a var name. Lexically declared names are only instantiated here but not initialized.
      b. For each element dn of the BoundNames of d, do
        i. If IsConstantDeclaration of d is true, then
          1. Perform ! lexEnv.CreateImmutableBinding(dn, true).
        ii. Else,
          1. Perform ! lexEnv.CreateMutableBinding(dn, false).
    

    These LexicallyScopedDeclarations are identifiers created with ES6+ syntax, and exclude var. (Identifiers created with var are classified as varNames, or VarDeclaredNames, which go through a different process than steps 33-34 above.)

    So, at the beginning of a block or function, class identifiers (and const and let identifiers) all have bindings that have been created in the environment, but have not been initialized in the environment. Initialization occurs when BindingClassDeclarationEvaluation runs, which does:

    5. Perform ? InitializeBoundName(className, value, env).
    

    This only happens when the engine is actually running the code inside the block or function and comes across class SomeClassName. (This is separate from the process referenced earlier, which is when the engine is only looking through the text of the block to get a list of identifiers that are declared inside.)


    When a block is running, and you try to instantiate something with new, EvaluateNew runs, which does, among other things:

    Let constructor be ? GetValue(ref).
    

    which eventually runs GetBindingValue, which has:

    2. If the binding for N in envRec is an uninitialized binding, throw a ReferenceError exception.
    

    The binding only gets initialized when InitializeBoundName runs - that is, when the engine comes across the class declaration.