TL;DR Don't do that. The semantics for it are complicated and (as you noted) they're different in strict mode vs. loose mode.
- Why is there no SyntaxError in the first case
Because that's just how JavaScript's var
and function declarations were designed back in 1995. Both create a "binding" (effectively a variable) in the function or global scope where they appear. If you use the same name for both, that same binding is reused. The function is assigned to it first (because function declarations are hoisted), but then when the step-by-step execution of the code occurs the a = 'foo'
part of var a = 'foo'
runs and overwrites the binding's value with 'foo'
.
second
Because you're declaring that function in a block, and the semantics are that are much newer (ES2015); prior to that, every JavaScript engine was free to handle that however it liked (and they handled them differently, and even the same engine would change how it handled them from one version to another).
The semantics there are that a let
-style binding is created at the top of the block and the function is assigned to it. Even though var
doesn't have block scope, you're not allowed to declare a var
variable and a let
variable with the same name in a block:
{
// Not allowed
let a = 1;
var a = 2;
}
When let
-style declarations were added, this was disallowed, even though (again) the var
isn't contained to the block.
With that in mind, here's roughly how the JavaScript engine interprets your "second" code:
// Uncaught SyntaxError: Identifier 'a' has already been declared
{
let a = function a() { };
var a = 'foo';
}
console.log(a);
Since that's the same situation (a var
-style declaration and a let
-style declaration in the same block), you're just not allowed to do that.
How the function a() {} overwrites var a?
It's that implicit local let
declaration again. Here's roughly how the JavaScript engine interprets that function declaration:
// **only no use strict!!!**
var a = 'foo';
console.log(a); // foo
{
// Block-local binding and function creation are hoisted
let a = function a() { };
console.log(a); // function a() {}
a<outer> = a<local>; // <======== Where the declaration was (not real syntax)
a = "bar"; // only the local is set
console.log(a); // bar
}
console.log(a); // function a() {}
Note that weird bit where the function declaration originally was:
a<outer> = a<local>; // Where the declaration was
The location of the declaration in the block controls when the value is assigned to the outer var
. Yes, this is weird. So the declaration and function creation are hoisted, and then assignment to the outer variable is done where the declaration appeared in the code (even though normally, the location of a function declaration relative to step-by-step code is irrelevant). Importantly, that assignment is from the local's current value as of where that code is reached in the step-by-step execution.
on this step - the global variable a = bar. Why is not function a() {}
Because the assignment is before the declaration in the block, and the local a
is copied to the outer a
where the declaration was. Here's roughly how the JavaScript engine handles that:
// **only no use strict!!!**
var a = 'foo';
console.log(a); // foo
{
let a = function a() { }; // Hoisted
console.log(a);
a = "bar";
a<outer> = a<local>; // <======== Where the declaration was (not real syntax)
console.log(a); // bar
}
console.log(a); // bar
Again, though, just don't do that. It's really complicated and arcane. I wrote an entire section on it for my recent book JavaScript: The New Toys (Chapter 3), and yet to answer your question I had to go back to that chapter and remind myself how this worked. Save those brain cells for something important.