Why does block assigned value change global variable? [duplicate]

The observed behavior is peculiar to non-strict mode, and Firefox does the same thing.

The reason it is behaving this way, is that it is following the Web Compatibility Semantics, as described in Annex B 3.3 in the spec.

The details are very complicated, but here is what the authors of this part of the engine implemented:

When an inner function a exists in a block, in sloppy mode, and when
the Web Compatibility Semantics apply (scenarios described in the
spec), then:

  1. Inner function a is hoisted with let-like block scope inside the block (“(let) a“)
  2. At the same time, a variable, also with name a, but with var semantics (ie. function scope), is created in the scope containing
    the block (“(var) a“)
  3. When the line declaring the inner function is reached, the current value of (let) a is copied into (var) a (!!)
  4. Subsequent references to name a from inside the block will refer to (let) a

So, for the following:

1:  var a = 0
2:  if(true) {
3:    a = 1
4:    function a() {}
5:    a = 2
6:  }
7:  console.log(a)

…this is what happens:

Line 1: (var) a is added the the variable environment of the outer scope, and 0 is assigned to it

Line 2: An if block is created, (let) a (ie. the function a() {}) is hoisted to the top of the block, shadowing (var) a

Line 3: 1 is assigned to (let) a (note, not (var) a)

Line 4: The value of (let) a is copied into (var) a, so (var) a becomes 1

Line 5: 2 is assigned to (let) a (note, not (var) a)

Line 7: (var) a is printed to the console (so 1 is printed)

The web compatibility semantics is a collection of behaviors that try to encode a fallback semantic to enable modern browsers to maintain as much backwards compatibility with legacy code on the Web as possible. This means it encodes behaviors that were implemented outside of the spec, and independently by different vendors. In non-strict mode, strange behaviors are almost expected because of the history of browser vendors “going their own way.”

Note, however, that the behavior defined in the specification might be the result of a miscommunication. Allen Wirfs-Brock said on Twitter:

In any case, the reported… result of a==1 at the end isn’t correct by any reasonable interpretation that I ever imagined.

and:

It should be function a(){}! Because within the block, all explicit references to a are to the block-level binding. Only the implicit B.3.3 assignment should go to the outer a

Final note: Safari, gives a totally different result (0 is printed by the first console.log, and 21 is printed by the final console.log). As I understand it, this is because Safari simply does not yet encode the Web Compatibility Semantics (example).

Moral of the story? Use strict mode.

More, details, here and here.

Leave a Comment