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).
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."
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):
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:
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...