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

2

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.

Rickettsia answered 15/7, 2015 at 1:15 Comment(2)
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…).Noach
Related.Canadian
L
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.

Lithotrity answered 16/7, 2015 at 18:14 Comment(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)?Rickettsia
@rvidal: Then the default block-level hoisting/scoping applies (in both cases) and the function is not visible on the outside of the block.Lithotrity
a few relevant links to (perhaps) update your answer github.com/estools/escope/issues/73 esdiscuss.org/topic/…Rickettsia
@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…Lithotrity
@Lithotrity Where I must search my problem? codepen.io/anon/pen/bMoYeZ?editors=0011Kat
@Kat Please ask a complete question and explain what your problem isLithotrity
Thank you for your answer @Bergi. When you say that the block-level function declaration (FD), compat, is hoisted to the scope of the outer function (enclosing), why is it also not initialised like a regular FD? I cannot reconcile this with the FunctionDeclarationInstantiation algorithm in ES2020. The steps I'm following are 10, 13, 14, 36. Step 36 initialises the FDs identified in step 10 - but hoisted block-level FDs are intialised as undefined, why is this?Jonme
Also, doesn't the initial hoisting to the outer scope also occur in pure ES? When enclosing is called, its scope is initialised by performing FunctionDeclarationInitialisation - isn't compat FD "hoisted" (i.e. a binding for compat is created inside enclosing's variable environment) at this point?Jonme
@Jonme "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.1Lithotrity
Right, but assuming strict were true, wouldn't the FD compat be included in VarScopedDeclarations (step 10), since the block is a part of enclosing's function code and would therefore be analysed for declarations when FunctionDeclarationInstantiation is called for enclosing?Jonme
@Jonme That list of varDeclarations does not contain non-var declarations from blocks. If I understand correctly, it even directly goes to TopLevelVarScopedDeclarations onlyLithotrity
That clears it up, thank you very much @Bergi.Jonme
But even in nodejs, non strict mode would enable function hoisting to enclosing function.Azotobacter
@Azotobacter Well no, not quite. Please read my answer again.Lithotrity
E
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.

Endearment answered 15/7, 2015 at 6:33 Comment(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]].Rickettsia
@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.Endearment
I don't quite follow yours either :P Let me rephrase a more specific question: Where in the spec can you infer what the value of strict should refer to in the B3.3 "additional steps" algorithm?Rickettsia
@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)Endearment
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 :-)Endearment
Thank you for trying again. I understand what you're saying, but I still see a logical gap which constitutes my main doubt. You said "The additional steps [...] start by a condition that the relevant block is not in strict mode.". How did you infer that? I only see that the first additional step reads "If strict is false...". There are no further mentions of strict mode at all in B3.3. Where is it stated what strict refers to, in this particular context? As I see it, the notation implies that strict is referring to a local variable introduced in 9.2.12, step 5.Rickettsia
@rvidal—my understanding of Appendix B3.3.3 is that it is "the semantic intersection of all of the browser implementations for such declarations", i.e. a description of use cases that are the same in all browsers pre ECMAScript ed6. I don't think it's meant to be a normative part of the specification.Noach
@rvidal - Let's try a demostrative path :-). look at this fiddle. Does that make you any more/less confused?Endearment

© 2022 - 2024 — McMap. All rights reserved.