Does this Safari behavior break the ECMAScript spe

2020-04-08 14:29发布

问题:

The following code prints 1 in Safari 13.0.4 on OSX.

let set = new Set

for(let x = 0; x < 2; x++) {
    function f() {}
    set.add(f)
}

console.log(set.size) // 1 in Safari non-strict mode

Also:

let set = new Set

for(let x = 0; x < 2; x++) {
    function f() {}
    f.test = x
    set.add(f)
}

console.log(set.size); // 1 in Safari
for(let x of set) console.log(x.test) // 1 in Safari non-strict mode

And:

let set = new Set;

for(let x = 0; x < 2; x++) {
    var v = (function () {})
    set.add(v);
}

console.log(set.size); // 2 in Safari non-strict mode

Is this behavior compatible with section 13.7.4.8 (see below) of the specification?

Note that: Node 13.9.0, Chrome 80.0.3987.122, and Brave 1.3.118 print 2.

13.7.4.8 of the spec:

(4.b seems pertinent)

The abstract operation ForBodyEvaluation with arguments test, 
increment, stmt, perIterationBindings, and labelSet is 
performed as follows:

1. Let V = undefined.

2. Let status be CreatePerIterationEnvironment(perIterationBindings).

3. ReturnIfAbrupt(status).

4. Repeat

  a. If test is not [empty], then

    i. Let testRef be the result of evaluating test.

    ii. Let testValue be GetValue(testRef).

    iii. ReturnIfAbrupt(testValue).

    iv. If ToBoolean(testValue) is false, return NormalCompletion(V).

  b. Let result be the result of evaluating stmt.

  c. If LoopContinues(result, labelSet) is false, return d.
     Completion(UpdateEmpty(result, V)).

  d. If result.[[value]] is not empty, let V = result.[[value]].

  e. Let status be CreatePerIterationEnvironment(perIterationBindings).

  f. ReturnIfAbrupt(status).

  g. If increment is not [empty], then

    i. Let incRef be the result of evaluating increment.

    ii. Let incValue be GetValue(incRef).

    iii. ReturnIfAbrupt(incValue).

回答1:

Yes, this is a bug in Safari[1]. However, as you noticed, it only occurs in global (or eval) scope and only in sloppy mode.

In general, these should definitely be distinct function instances, not getting hoisted outside of the block. However, Safari - being a browser - does implement the Block-Level Function Declarations Web Legacy Compatibility Semantics from Annex B3.3 of the specification (see here for details). In ES6 and ES7, these did apply only to block statements inside functions though. Only since ES8, they are also specified for declaration instantiations in global and eval scopes.

It seems that Safari did not adopt that change from ES8 yet, and has kept their own (noncompliant) pre-ES6 semantics for block-scoped declarations in the global scope, where they hoist the declaration completely.

1: Probably #201695 or #179698. "We don't support this in global scope. We do support it inside functions and I believe eval. We still need to implement it for the global scope."



回答2:

To my understanding, code that has a function declaration placed within a block, should follow the specification of 13.2.14 (I put in bold):

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.

One of the steps deals with function declarations explicitly, which depends on InstantiateFunctionObject, which in turn depends on OrdinaryFunctionCreate, OrdinaryObjectCreate, MakeBasicObject ... which creates a new object.

All this happens at the evaluation. Your quote from the specifications dictate that the evaluation happens for each iteration, and so the function object should be newly created in each iteration.

Differences in implementation

The specification has a section on implementation differences related to block-level function declarations. It says:

Prior to ECMAScript 2015, the ECMAScript specification did not define the occurrence of a FunctionDeclaration as an element of a Block statement's StatementList. However, support for that form of FunctionDeclaration was an allowable extension and most browser-hosted ECMAScript implementations permitted them. Unfortunately, the semantics of such declarations differ among those implementations. Because of these semantic differences, existing web ECMAScript code that uses Block level function declarations is only portable among browser implementation if the usage only depends upon the semantic intersection of all of the browser implementations for such declarations. The following are the use cases that fall within that intersection semantics:

  1. A function is declared and only referenced within a single block

    • One or more FunctionDeclarations whose BindingIdentifier is the name f occur within the function code of an enclosing function g and that declaration is nested within a Block.
    • No other declaration of f that is not a var declaration occurs within the function code of g
    • All occurrences of f as an IdentifierReference are within the StatementList of the Block containing the declaration of f.

Now the case in your question behaves according to specification (print 2) when the code is not a top-level script, but placed in a function body. In that case we are in situation 1 (in the above quote). But this point is not applicable when the script is global. And so, we see indeed deviating behaviour...