36

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.

2
  • 3
    Please forget the term "hoisting". All function declarations are processed before any code is executed. Block level scoping affects how identifier resolution occurs (i.e. nothing to do with "hoisting"), a function declared within a block may or may not be available outside the block. Oh, and function declarations are processed after variable declarations, so they overwrite variables (of course a later assignment to the variable can change that…).
    – RobG
    Jul 15, 2015 at 6:19
  • Related.
    – Ben Aston
    Apr 14, 2020 at 16:32

2 Answers 2

50

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.

14
  • Excellent answer! What would it happen then when: (1) the function declaration is not "sane"? or (2) the function declaration is "top-level-block-level" (i.e. the containing block is top-level instead of nested within a function)?
    – rvidal
    Jul 17, 2015 at 15:39
  • 1
    @rvidal: Then the default block-level hoisting/scoping applies (in both cases) and the function is not visible on the outside of the block.
    – Bergi
    Jul 17, 2015 at 15:55
  • 1
    @rvidal: The first doesn't change my answer :-) The second does refer to an errata in the ES6 spec about this legacy behaviour only applying to function scopes (which it shouldn't), but I didn't distinguish that in my answer anyway. Btw, the first link in my first paragraph already mentions this…
    – Bergi
    Aug 27, 2015 at 20:38
  • 1
    @user51462 "Step 36 initialises the FDs identified in step 10" - but that does not include function declarations from inner blocks. Their hoisting to the function level is governed by §B.3.3.1
    – Bergi
    Jun 29, 2021 at 9:43
  • 1
    @user51462 That list of varDeclarations does not contain non-var declarations from blocks. If I understand correctly, it even directly goes to TopLevelVarScopedDeclarations only
    – Bergi
    Jun 29, 2021 at 11:17
3

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.

8
  • Are you sure about that? I think strict in B3.3 refers to a local variable previously introduced in the algorithm description in 9.2.12. And within that algorithm, strict == func.[[Strict]].
    – rvidal
    Jul 15, 2015 at 13:54
  • 2
    @rvidal I really don't understand your logic. Consider this { var f, isStrict = IsInStrict(); if(!isStrict) { f = function() {'use strict'} } }. Now, suppose IsInStrict "works" (there are some suggestions how to test that on SO), if not in strict mode, by your logic going into the if statement would turn the outside scope into strict mode. That's not logical, and in fact creates a paradox.
    – Amit
    Jul 15, 2015 at 14:17
  • 2
    @rvidal - Let's try this again :). Annex B is about legacy features that are NOT part of the standard. They are listed to describe behavior that is non-standard, but is required in order to maintain compatibility with older code. B3.3 describe one such feature, namely "Block Level Function Declarations". The additional steps are ones to consider at step 29 of 9.2.12, and start by a condition that the relevant block is not in strict mode. In your sample, that does relate to a function declared inside a block, the block is indeed non-strict (by virtue of not explicitly being strict) (cont. next)
    – Amit
    Jul 15, 2015 at 20:59
  • 2
    What follows are a set conditions and behaviors that stray from the standard and define an alternative behavior who's purpose is to allow said legacy code to function as originally designed. If you'd look at 9.2.12 step 29 you'd see this step refers back to B3.3 for details in non-strict mode. If your original block was in strict mode (for example if it starts with 'use strict'), this entire step is skipped and normal, standard behavior is applied. I hope that's a bit more clear, cause if not, I don't think I'll be able to give you a satisfactory answer (sorry then :-)
    – Amit
    Jul 15, 2015 at 21:06
  • 1
    @rvidal - Let's try a demostrative path :-). look at this fiddle. Does that make you any more/less confused?
    – Amit
    Jul 16, 2015 at 8:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.