16

var a = 0;
if (true) {
  console.log(a)
  a = 1;

  function a() {}
  a = 21
  console.log(a)
}
console.log(a)

In my opinion, because function declaration hoisting, a = 1 and a = 21 will change local function variable ,so in block will output 21,and outside is 0,but the true result is outside output 1.

Debug with chrome, result like that

When running on function a() {} ,it will change local and global variable. so weird? Who can give me some explanation?

terrymorse
  • 6,771
  • 1
  • 21
  • 27
ZenHeart
  • 169
  • 5
  • 2
    I suspect this is due to the [Web Compatibility Semantics](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-block-level-function-declarations-web-legacy-compatibility-semantics) coming into effect in non-strict mode. See [also](https://stackoverflow.com/a/31461615/38522). – Ben Aston Apr 13 '20 at 16:27
  • 2
    @52d6c6af Yup. However, the behaviour I described in the answer you linked doesn't fit the observed behaviour here. So either it's to do with special-casing web compat semantics if an upper-level variable of the same name already exists, or it's a bug in Chrome. – Bergi Apr 13 '20 at 17:28
  • 1
    Firefox does the same thing. Safari doesn't (first `console.log` prints `0`, and final `console.log` prints `21`). But I happen to be aware of a bug in Safari to do with function declarations in blocks. This suggests that this behavior might be deliberate. Maybe. – Ben Aston Apr 13 '20 at 17:40
  • 2
    Ongoing discussion... https://twitter.com/03d5d0a1/status/1249777008307044355?s=21 – Ben Aston Apr 13 '20 at 19:58

4 Answers4

13

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.

Ben Aston
  • 53,718
  • 65
  • 205
  • 331
  • 2
    Ah, perfect, this behaviour still matches [my explanation](https://stackoverflow.com/a/31461615/1048572) in the other thread. It just never occured to me (and apparently, not to the spec editors either) that the block-scoped variable would not hold the function value but something else when it gets copied into the outer-scope variable. – Bergi Apr 14 '20 at 15:06
  • 1
    I think a wire was crossed somewhere. I think the expectation was that the function object would be assigned to the outer `var` at the point of declaration of the inner function. What was implemented was for the value associated with the identifier to be copied at that point, which is subtly different. – Ben Aston Apr 14 '20 at 16:28
  • @BenAston, so why "**The value of (let) a is copied into (var) a**" happens at the declaration of inner function, why not just under **(let) a = function() {}** above **(let) a = 1**? – Marvin Dev Dec 22 '21 at 10:27
0

This is also "interesting". It seems that a = 21 assigns a to the global var, and the function reference is unreachable.

var a = 0;
if (true) {
  function a() {}
  console.log(typeof a, a) // number 0
  a = 21;
}
console.log(typeof a, a) // number 21
terrymorse
  • 6,771
  • 1
  • 21
  • 27
-1

a = 1; reassigns to the original variable, making its value 1. function a() {} masks a and "takes over" a only in the if condition. Read more about shadowing and masking.

twharmon
  • 4,153
  • 5
  • 22
  • 48
  • 1
    This is correct. Also `function` declaration statements inside conditional code blocks are abominations. – Pointy Apr 13 '20 at 17:03
  • 4
    This is not a sufficient explanation, because this is not normal behavior. The function in the block should be hoisted, meaning the `a = 1` affects the inner `a`. In strict mode the behavior is as expected. It’s almost as if the shadowing only comes into effect after the function declaration. – Ben Aston Apr 13 '20 at 17:18
  • 2
    @Pointy Consider how in strict mode, it logs `21` and `0` as expected. There's something else going on. – Bergi Apr 13 '20 at 17:29
  • @Bergi agreed now that I look at it, and I agree that it has to be about compatibility. In a world where such a `function` declaration might be hoisted to the enclosing function or global scope, the `var` declaration outside the `if` would have shadowed the function. Maybe it's a result of some kind of compromise. – Pointy Apr 13 '20 at 17:38
  • And I still think that `function` declarations shouldn't be in conditional blocks :) – Pointy Apr 13 '20 at 17:42
-2

Hy there. Let's look at it this way, when you declare a variable named 'a=1' (the variable you declared is a global one) , a memory house is assigned to a variable that has this value: 1
and after you declare the function: function a() {} there is no memory house in type of variable for 'a' anymore and the a is a function. but, the memory house for 'a' doesn't cleared and it stills available and it is like the a function called once and returned the last assigned value! so, when you write this codes:

  a = 1;
  function a() {}

it is just like:

function a(){
return 1;
}
console.log(a());

this problem is like because of a Incomplete nature(i mean memory house type) change.

Pooya Behravesh
  • 131
  • 2
  • 10