case 1
var a // ===> undefined
let a // ===> SyntaxError: Identifier 'a' has already been declared
case 2
a = 1 // ===> 1
var a // ===> undefined
let a // ===> undefined
Why case 2 does not throw an exception?
V8 developer here.
The first thing worth pointing out is that there are differences between regular script execution and "console mode" (a.k.a. "REPL mode"). The former is specified by the JavaScript standard, so all engines should behave the same there; otherwise it's a bug. The latter is not specified; it should obviously be similar to the former, but due to the interactive usage it turns out that it makes sense to have some minor differences.
Let's look at a few concrete examples and explain what's going on.
(1) Re-declaring a let
-variable is invalid in regular script. In the DevTools console, that turns out to be annoying though:
> let a = 42j // typo: I meant "42"
< Uncaught SyntaxError: Invalid or unexpected token
> let a = 42 // Let's try that again
< Uncaught SyntaxError: 'a' has already been declared
// Aaaargh!!!
So a while ago Chrome made some changes to special-case the behavior around let
-redeclarations for the console, which is why you can now see the following difference:
> let a; let a; // One script: redeclaration error
> let b
> let b // Separate evaluations: OK, no error.
If you executed the last two lines as part of regular script execution, you'd get the same error as when putting both declaration on the same line in DevTools.
(2) var
declarations vs. implicit global variables: there is a small difference between c = 1
and var d = 1
: the former creates a configurable property on the global object, the latter creates a non-configurable property, so while the two are almost equivalent, they can still be distinguished afterwards. Now, in regular script execution, var
declarations are hoisted, so if you have an explicit declaration following an implicit use of the same variable:
e = 1;
var e = 2;
then this gets transformed internally to: var e; e = 1; e = 2
. So in this case, you can't observe the configurability difference.
In console mode, the exact same behavior isn't possible: when you execute e = 1
, the engine can't know yet that you're going to type var e = 2
next. So it creates e
as a configurable property of the global object. When var e
follows, it doesn't do anything, because the e
property already exists.
(3) let
variables vs. global object properties: variables declared with let
do not create properties on the global object, they are in their own scope, shadowing the global property. Here's how you can observe that effect (both in regular script and in the console):
f = 1
let f = 2
console.log(window.f, f) // prints "1 2"
(4) Limitations to redeclarations in REPL mode: when REPL mode with its loosened restrictions on normally-invalid redeclarations was built for V8, the team decided to (continue to) disallow redeclarations that would change the kind (var
, let
, const
) of a variable, as well as redeclaration of consts:
> var g
> var g // OK
> let g // error: already declared
> const g = 1 // error: already declared
> let h
> let h // OK (REPL-mode special case)
> var h // error: already declared
> const h = 1 // error: already declared
> const i = 1
> const i = 2 // error: already declared
> var i // error: already declared
> let i // error: already declared
(So there's only one difference to regular script execution here, see the comment on that line above.)
Armed with this knowledge, we can now get back to your original question:
var a // Creates a variable of `var` kind.
let a // Redeclaration -> error
a = 1 // Creates global object property.
var a // Does nothing, because `a` already exists.
let a // Creates a variable of `let` kind, shadowing global property.
TL;DR: Use let
for everything, and your code will behave reasonably.
var
has weird semantics that can't be fixed because it would break backwards compatibility with existing websites. Forgetting the keyword entirely gives even weirder behavior, especially once functions are involved.