What are the precise semantics of block-level functions in ES6?

I'm trying to wrap my head around the new standardized block-level functions in ES6 by reading the raw spec. My superficial understanding was:

  • Block-level functions declarations are allowed in ES6.
  • They hoist to the top of the block.
  • In strict mode, they aren't visible outside the containing block.

However, this is further complicated by the fact that part of these semantics are specified to be "optional" and only mandatory for web browsers (Annex B). So I would like have the following table filled:

                                             |  Visible outside of block?  |  Hoisted? Up to which point?  |   "TDZ"? |
------------------------------------------------------------------------------------------------------------------------
|   Non-strict mode,   no "web extensions"   |                             |                               |          |
|   Strict mode,       no "web extensions"   |                             |                               |          |
|   Non strict mode,   with "web extensions  |                             |                               |          |
|   Strict mode,       with "web extensions" |                             |                               |          |

Also it is unclear to me what "strict mode" means in this context. This distinction seems to be introduced in Annex B3.3, as part of some additional steps for the runtime execution of a function declaration:

1. If strict is false, then
...

However, as far as I can see, strict refers to the [[Strict]] internal slot of the function object. Does this mean that:

// Non-strict surrounding code

{
    function foo() {"use strict";}
}

should be considered "strict mode" in the table above? However, that's contradicts my initial intuition.

Please, bear in mind that I'm mostly interested in the ES6 spec itself, regardless of actual implementation inconsistencies.


Solution 1:

As far as I can see, strict refers to the [[Strict]] internal slot of the function object.

No. And yes. It does refer to the strictness of the function (or script) in which the block that contains the function declaration occurs. Not to the strictness of the function that is (or is not) to be declared.

The "web extensions" do only apply to sloppy (non-strict) code, and only if the appearance of the function statement is "sane" - that is, for example, if its name doesn't collide with a formal parameter or lexically declared variable.

Notice that there is no difference between strict and sloppy code without the web-compatibility semantics. In pure ES6, there is only one behaviour for function declarations in blocks.

So we basically have

                 |      web-compat               pure
-----------------+---------------------------------------------
strict mode ES6  |  block hoisting            block hoisting
sloppy mode ES6  |  it's complicated ¹        block hoisting
strict mode ES5  |  undefined behavior ²      SyntaxError
sloppy mode ES5  |  undefined behavior ³      SyntaxError

1: See below. Warnings are asked for.
2: Typically, a SyntaxError is thrown
3: The note in ES5.1 §12 talks of "significant and irreconcilable variations among the implementations" (such as these). Warnings are recommended.

So now how does an ES6 implementation with web compatibility behave for a function declaration in a block in a sloppy-mode function with legacy semantics?
First of all, the pure semantics still apply. That is, the function declaration is hoisted to the top of the lexical block.
However, there is also a var declaration that is hoisted to the top of the enclosing function.
And when the function declaration is evaluated (in the block, as if it was met like a statement), the function object is assigned to that function-scoped variable.

This is better explained by code:

function enclosing(…) {
    …
    {
         …
         function compat(…) { … }
         …
    }
    …
}

works the same as

function enclosing(…) {
    var compat₀ = undefined; // function-scoped
    …
    {
         let compat₁ = function compat(…) { … }; // block-scoped
         …
         compat₀ = compat₁;
         …
    }
    …
}

Yes, that's a bit confusing, having two different bindings (denoted with the subscripts 0 and 1) with the same name. So now I can succinctly answer your questions:

Visible outside of block?

Yes, like a var. However, there's a second binding that is visible only inside the block.

Hoisted?

Yes - twice.

Up to which point?

Both to the function (however initialised with undefined) and the block (initialised with the function object).

"TDZ"?

Not in the sense of the temporal dead zone of a lexically declared variable (let/const/class) that throws on referencing, no. But before the function declaration is encountered in the execution of the body, the function-scoped variable is undefined (especially before the block), and you'll get an exception as well if you try to call it.


Just for reference: in ES6, the above-described behaviour was specified only for blocks in function scopes. Since ES7 the same applies to blocks in eval and global scopes.

Solution 2:

I'm not sure where your confusion comes from. According to 10.2.1 it's very clear what is or isn't "in strict mode". In your sample, foos [[Strict]] internal slot would be true indeed and will be in strict mode, but the block hosting it will not. The first sentence (the one you quoted) relates to the hosting block, not the content generated within it. The block in your fragment is not in strict mode and hence that section applies to it.