Reading the ECMA-262: How Declarations in Block Context Work

Published on

There are four kinds of context in which variable or function declarations would occur: Global, module, function, and block. This article focuses on the block context.

Steps in Specification of Declarations in Block Context

The specification of Evaluation of blocks and BlockDeclarationInstantiation are not as complicated as the function or global ones. So we can look into the source of specification directly:

14.2.2 Runtime Semantics: Evaluation Block : { StatementList }

  1. Let oldEnv be the running execution context’s LexicalEnvironment.
  2. Let blockEnv be NewDeclarativeEnvironment(oldEnv).
  3. Perform BlockDeclarationInstantiation(StatementList, blockEnv).
  4. Set the running execution context’s LexicalEnvironment to blockEnv.
  5. Let blockValue be the result of evaluating StatementList.
  6. Set the running execution context’s LexicalEnvironment to oldEnv.
  7. Return blockValue.

14.2.3 BlockDeclarationInstantiation ( code, env ) The abstract operation BlockDeclarationInstantiation takes arguments code (a Parse Node) and env (a declarative Environment Record) and returns unused. code is the Parse Node corresponding to the body of the block. env is the Environment Record in which bindings are to be created. Note: When a Block or CaseBlock is evaluated a new declarative Environment Record is created and bindings for each block scoped variable, constant, function, or class declared in the block are instantiated in the Environment Record.

  1. “ Let envRec be env’s EnvironmentRecord.
  2. Assert: envRec is a declarative Environment Record.
  3. Let declarations be the LexicallyScopedDeclarations of code.
  4. For each element d in declarations, do For each element dn of the BoundNames of d, do If IsConstantDeclaration of d is true, then Perform ! envRec.CreateImmutableBinding(dn, true). Else, Perform ! envRec.CreateMutableBinding(dn, false). If d is a FunctionDeclaration, a GeneratorDeclaration, an AsyncFunctionDeclaration, or an AsyncGeneratorDeclaration, then Let fn be the sole element of the BoundNames of d. Let fo be InstantiateFunctionObject of d with argument env. Perform envRec.InitializeBinding(fn, fo).

According to the spec, BlockDeclarationInstantiation does not include var statements (VariableStatement), as these are not part of LexicallyScopedDeclarations. The LexicallyScopedDeclarations include let, const, function, and class.

As mentioned in the previous articles, the VariableStatements in nested blocks have been collected into VarScopedDeclarations when processing global or function declaration instantiations.

And the BlockDeclarationInstantiation in the block are bonded to the newly created declarative Environment Record, whose environment’s outer environment is the running execution context’s LexicalEnvironment

Test in the Hosts

We can test the behavior in actual hosts. The following pieces of code are tested in the hosts with the same result:

  • Edge 127.0.2651.86
  • Firefox 129.0
  • Safari 17.2.1
  • Node v18.17.1

function declarations

function declarationsinside the block should NOT be hoisted out of that block:

  • In a global context:

    "use strict";
    foo("before block"); // ReferenceError: foo is not defined
    {
    foo("in block before declaration");
    function foo(position) {
    console.log("Block scoped function", position);
    }
    foo("in block after declaration");
    }
    foo("after block");
  • In a function context:

    "use strict";
    function test() {
    foo("before block"); // ReferenceError: foo is not defined
    {
    foo("in block before declaration");
    function foo(position) {
    console.log("Block scoped function", position);
    }
    foo("in block after declaration");
    }
    foo("after block");
    }
    test();

var declarations

var declarationsinside the block should be hoisted out of that block and initialized with value undefined:

  • In a global context:

    "use strict";
    console.log("before block", bar); // undefined
    {
    console.log("in block before declaration", bar); // undefined
    var bar = 30;
    console.log("in block after declaration", bar); // 30
    }
    console.log("after block", bar); // 30
  • In a function context:

    "use strict";
    function test() {
    console.log("before block", bar); // undefined
    {
    console.log("in block before declaration", bar); // undefined
    var bar = 30;
    console.log("in block after declaration", bar); // 30
    }
    console.log("after block", bar); // 30
    }
    test();