5

With the following code, which is neither ES6, nor is it in "strict mode", I had expected an outcome of 'b' because the second declaration of foo should overwrite the first one. But the outcome is 'a'!

{
  function foo() {
    console.log('a');
  }
}

function foo() {
  console.log('b');
}

foo(); // 'a' ? Why not 'b'?

When this code is surrounded with additional curly braces, the outcome is the expected 'b'.

{ // additional curly braces

  {
    function foo() {
      console.log('a');
    }
  }

  function foo() {
    console.log('b');
  }

  foo(); // 'b' as expected!

} // end additional curly braces 

For further illustration, please consider the following additional example:

foo('before declaration'); // outcome:  from outside block :before declaration

{
  function foo(s) {
    console.log('from inside block: ' + s);
  }
}

function foo(s) {
  console.log('from outside block :' + s);
}

foo('after declaration'); // outcome:  from inside block: after declaration

In my opinion the correct outcome should be:

// from outside block :before declaration
// from outside block :after declaration

I'm unable to spot my misconception here.

If I again enclose the complete last example inside curly brackets like so:

{
  foo('before declaration'); // outcome:  from outside block :before declaration

  {
    function foo(s) {
      console.log('from inside block: ' + s);
    }
  }

  function foo(s) {
    console.log('from outside block :' + s);
  }

  foo('after declaration'); // outcome:  from outside block: after declaration
}

I get the expected outcome.

Darryl Noakes
  • 2,207
  • 1
  • 9
  • 27
micmor
  • 51
  • 3
  • I have pasted your first example in an empty file and it returns 'b' as expected and not 'a' – klugjo Dec 05 '17 at 07:27
  • @klugjo - which browser? – Jaromanda X Dec 05 '17 at 07:31
  • 1
    note the difference if you `"use strict"` – Jaromanda X Dec 05 '17 at 07:39
  • I've tested under Windows10 with Google Chrome and Firefox, both the same effect. – micmor Dec 05 '17 at 07:40
  • No difference while using strict-mode. – micmor Dec 05 '17 at 07:43
  • really? maybe you've put "use strict" in the wrong place :p compare https://jsfiddle.net/2jytw2mq/ with https://jsfiddle.net/2jytw2mq/1/ – Jaromanda X Dec 05 '17 at 07:45
  • I've done that in the same way - no difference! The point is, that the surrounding curly braces shouldn't change scope, thus the outcome should be 'b' regardless of the surrounding curly braces. I had expected it would be equivalent to declaring this duplicate functions without the nesting shown in the first example. The outcome of the second part of my example makes my confusion perfekt. – micmor Dec 05 '17 at 09:06
  • I hope the duplicate explains well enough why this is happening in web compatibility mode - the declaration in the block will overwrite the outer (function-scope) variable when evaluated. To fix the behaviour, use strict mode. – Bergi Dec 05 '17 at 10:21
  • "*With the following code, wich is not ES6*" - but it very much looks like it, because prior to ES6 function declarations in blocks were completely invalid, and only ES6 specified how to treat them in sloppy web-compat mode. Please state exactly in which environment you are executing this if not a modern (ES6-compatible) web browser. – Bergi Dec 05 '17 at 12:05
  • @Bergi Thats obviously true and not the question here. Why doesn't your answer apply to the second part of my example code? I have extended my example with another peace of code. Here both declarations come into effect. – micmor Dec 05 '17 at 12:05
  • @micmor The ES6 (sloppy mode, web compat) behavior as detailed in my answer to the duplicate explains your second snippet as well. Inside the block, the respective ("hoisted") declaration is in effect. The "overwriting" behaviour of block-scoped declarations takes effect only on the (implicit) variable in the top scope – Bergi Dec 05 '17 at 12:09
  • @Bergi Sorry, you haven't answered my question. Unfortunately, I can't recognize consistent logic inside your answer, especially not in relation to my extended example (third part). May be, my question is wrong. I think, I have to find the correct answer by myself. All is fine, have a good day. – micmor Dec 05 '17 at 12:32
  • OK, I'll write a complete answer. – Bergi Dec 05 '17 at 12:36

3 Answers3

1

In your first declaration, you have enclosed foo() within {} , which changes the scope for the declaration, but the next declaration is the global scope. From w3schools JavaScript Hoisting,

Hoisting is JavaScript's default behavior of moving all declarations to the top of the current scope

So your second declaration is actually moved to top, and then the declaration inside the scope {} executes, which overrides the first declaration and prints a.

sSD
  • 282
  • 2
  • 17
  • does "w3schools" explain why strict-mode changes this behaviour? – Jaromanda X Dec 05 '17 at 07:49
  • Need to check on that! – sSD Dec 05 '17 at 07:51
  • This is strange indeed because javascript does not have block scope. Only function scope. So the extra `{}` should not, in theory, change scope – slebetman Dec 05 '17 at 07:51
  • 2
    Why doesn't this apply to the second part of my code example. That's what confuses me. – micmor Dec 05 '17 at 07:56
  • @slebetman [Sure it does](https://stackoverflow.com/q/31419897/1048572) – Bergi Dec 05 '17 at 10:06
  • Your answer doesn't make sense. If enclosing the first declaration in a block does change its scope (which it does indeed), it would *not* override the declaration in the outer scope. – Bergi Dec 05 '17 at 10:22
  • @Bergi , What I meant was the function declaration in the outer scope is hoisted first, and then is overridden by the second declaration :). But yes, why this doesn't apply to the second part of the code example is confusing and needs further understanding, if you can help on this. – sSD Dec 05 '17 at 10:32
  • @sSD Even if that's what you meant, it's not what is happening. a) functions are hoisted in order of appearance, the first is overridden by the second not the other way round b) functions are hoisted in their own scope, which is the block scope for the one function in the example. They cannot collide during the hoisting. – Bergi Dec 05 '17 at 10:53
  • @sSD For further understanding, see my answer to the duplicate :-) – Bergi Dec 05 '17 at 10:53
  • @Bergi If the scope would change, I shouldn't be able to access foo outside this respective scope, but I can. Ommit the second declaration in the first part of my example and you will see! No changed scope there!! – micmor Dec 05 '17 at 13:27
  • @micmor *In strict mode* (which you should normally be using), that is indeed the case. Why in sloppy mode you still can access the functions outside of their usual scopes is explained in my answers. – Bergi Dec 05 '17 at 13:29
1

I had expected an outcome of 'b' because the second declaration of foo should overwrite the first one.

No, it's not that the second declaration should overwrite the first one. The expected outcome would be 'b' because the first declaration is contained in its own (block) scope and would not affect the outer scope.

But the outcome is 'a'! Why not 'b'????

Because you are not executing the code in strict mode (you really should!). And in sloppy mode, web browsers behave weird to be compatible with older engines that treated the then-illegal function statements in ES5 specially.

So what is happening here? The block-scoped function declaration does, when the statement is evaluated, assign the function to a top-level variable of the same name. (That happens in addition to the normal scoped (and "hoisted") declaration).

Your first example therefore behaves like

var foo₀; // the top-level implicit introduction of the inner declaration
var foo₀ = function foo() { console.log('b'); } // the hoisted declaration
foo()₀; // logs 'b'
{
  let foo₁ = function foo() { console.log('a'); } // the (hoisted) inner declaration
  foo₀ = foo₁; // the weirdness!
  // a call to `foo()` in here would get you foo₁ from the local scope
}
foo()₀; // logs 'a'

Your second example behaves as

var foo₀; // the top-level implicit introduction of the first declaration
var foo₀; // the top-level implicit introduction of the second declaration
{
  let foo₁ = function foo() { console.log('b'); } // the (hoisted) second declaration
  foo()₁; // logs 'b'
  {
    let foo₂ = function foo() { console.log('a'); } // the (hoisted) first declaration
    foo₀ = foo₂; // the weirdness!
    // a call to `foo()` in here would get you foo₂ from the local scope
  }
  foo₀ = foo₁; // the weirdness!
  foo₁() // logs 'b' (as expected) - a call in here does get you foo₁ from the local scope
}
// a call to `foo()` out here would get you foo₀ from the top scope
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    Thank you for your detailed explanation. Written in this way, the matter is perfectly clear to me. The strange behavior is due to sloppy mode and therefore lacks a consistent logic. – micmor Dec 05 '17 at 13:51
  • I have a problem with "because the first declaration is contained in its own (block) scope and would not affect the outer scope.". Is this correct? Because `function` and `var` exist in the scope of the parent function or file, not in the blocks defined by braces. The same is not true for `let` and `const`, which would not exist outside the `{}` block. In fact, `{function foo(){}}` makes `foo()` exist in the file/parent function. – E. Zacarias Apr 25 '23 at 17:58
  • 1
    @E.Zacarias No, `function` declarations exist in the block scope. If you are not using strict mode (though you really should!), they also introduce an *additional* global/function-scoped variable. This is required from engines to [provide web compatibility](https://stackoverflow.com/a/31461615/1048572) for legacy code that relies on weird hoisting behaviour. – Bergi Apr 25 '23 at 18:36
-1

Having multiple definitions of the same function in a single scope is not supported unless they are at top level.

The reason is that there is a big difference between for example:

function square(x) { return x*x; }

and

var square = function(x) { return x*x; }

where in the first case the name square is bound to the function object at start and not when the definition is "executed" during the normal program flow:

function foo() {
    console.log(square(12));  // Valid, no prob the function is defined "later"
    function square(x) { return x*x; }
}

this however implies that if you place different definitions in the same scope it's not clear what should be done.

The standard describes what should happen only for multiple definitions at top level (last wins).

What to do in nested parts is instead not defined and different implementations (e.g. Firefox and Chrome) do not agree. What is clearly nonsense is for example placing two definitions for the same function in the two branches of an if-else statement (remember that the name binding must happen immediately, before statements in the scope begin execution).

Just don't do that.

6502
  • 112,025
  • 15
  • 165
  • 265
  • 1
    actually, all browsers I've tested, and nodejs, behave identically - so `Different implementations handle this in different ways` seems to be more of an opinion than a fact :p – Jaromanda X Dec 05 '17 at 07:37
  • not sure how your square function demonstrates anything relevant to the question, to be honest – Jaromanda X Dec 05 '17 at 07:47
  • function foo() { console.log('a'); } function foo() { console.log('b'); } foo(); // b – micmor Dec 05 '17 at 07:47
  • In practice I never do things like that. But in theory duplicate function declarations are allowed and the last one should overwrite any previous one. Why is this not the case for my code example? The surrounding curly braces shouldn't change the scope!! Obviously I didn't understand the mechanism as a whole! – micmor Dec 05 '17 at 08:14
  • @micmor: the standard describes what should happen only for multiple definitions *at top level*. What to do in nested parts is instead not defined and different implementations (e.g. Firefox and Chrome) do not agree. What is clearly nonsense is for example placing two definitions for the same function in the two branches of an `if` statement (remember that the name binding must happen immediately, before statements in the scope begin execution). – 6502 Dec 05 '17 at 08:22
  • "*Having multiple definitions of the same function in a single code block*" - that's not the case here. The OP's example has two function declarations in two (nested) blocks. – Bergi Dec 05 '17 at 10:18
  • @6502 Actually the standard is quite complete and covers all those cases. – Bergi Dec 05 '17 at 11:13
  • @Bergi Please note that the blocks should not change the scope in any way and obviously do not. – micmor Dec 05 '17 at 13:22
  • @micmor Of course the blocks do introduce new block scopes? – Bergi Dec 05 '17 at 13:27