var vs. let vs. const
JavaScript provides in total three keywords—var
, let
, const
—to be able to declare variables. This post covers difference between three keywords in detail.
Cheat Sheet
var |
let |
const |
|
scope | global scope/ function scope |
block scope | block scope |
beginning of a ... | statement | declaration | declaration |
declaration w/o value initialization | 🟢 (default undefined ) |
🟢 (default undefined ) |
❌ Syntax Error |
re-declaration | 🟢 | ❌ | ❌ |
re-assignment | 🟢 | 🟢 | ❌ |
hoisting | 🟢 | ❌ | ❌ |
access before declaration | undefined |
Reference Error | Reference Error |
Scope
Variables declared using var
are either function-scoped or global-scoped. If it's created outside of any function, it's added as a property to global object.
var a = 1
let b = 2
const c = 3
console.log(a) // 1
console.log(b) // 2
console.log(c) // 3
console.log(window.a) // 1
console.log(window.b) // undefined
console.log(window.c) // undefined
console.log(window.a === a) // true
If var
is inside a function, the variable it created is accessible anywhere within the function it was created. On the other hand, let
and const
are block-scoped, meaning that they are only accessible within the nearest set of curly braces.
function () {
if (true) { // block scope
var a = 1
let b = 2
const c = 3
}
console.log(a); // 1
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined
}
In the above code, when a variable is declared within a function using var
, it is accessible outisde if
block, as long as it is in the function scope it was created. On the other hand, reference to b
and c
throws Reference Error since let
and const
are only valid within block scope of if
statement.
Statement vs. Declaration
var
keyword designates beginning of statement, whereas let
and const
launches declaration. This means that var
keyword may appear in some part of code where let
and const
may not.
For example, most control flows such as if ... else
block only accept statement in body. Therefore, these code snippets here throws Syntax Error,
if (true) let a = 0;
// SyntaxError: Lexical declaration cannot appear in a single-statement
if (true) const a = 1
// Uncaught SyntaxError: Unexpected token 'const'
while this doesn't:
if (true) var i = 0;
According to MDN, declarations are those that "bind[] identifiers to values while statements "carr[y] out actions." In this regard, it seems odd that var
are thought to start a statement while let
and const
declaration. In light of what MDN goes on to say, we can see this as reflecting var
not following normal lexical scoping rules and creating side effects such as "creating global variables, mutating existing (...) variables, and defining variables that are visible outside its block."
Declaration Without Value Initialization
When declaring variable with var
or let
, initialization of value may be skipped. If no value is specified, it is initialized with default value undefined
. However, when declaring constant variable with const
, initializer is required and value must be specified as it is declared, since constants, by definition, do not allow changes to value after declaration.
var a
console.log(a) // undefined
let b
console.log(b) // undefined
const c // Uncaught SyntaxError: Missing initializer in const declaration
Re-Declaration
Re-declaring a variable with the same name as existing var
-declared variable doesn't throw error. The one declared later overrides the value of previous one. The same is true with use strict
.
var a = 'oldVal'
var b = 'newVal'
console.log(b) // newVal
Re-Assignment
var
and let
both allows re-assigning value to existing variable while const
does not.
var a = 10
let b = 20
const c = 30
a = 'a'
b = 'b'
c = 'c' // Uncaught TypeError: Assignment to constant variable.
On the other hand, there can't be more than one let
- or const
- declared variable with same name within a scope.
let x = 10
let x = 15 // Uncaught SyntaxError: Identifier 'x' has already been declared
const y = 10
const y = 15 // Uncaught SyntaxError: Identifier 'y' has already been declared
let
and const
also prevents var
keyword from declaring variable with same identifier
let x = 10
var x = 10000 // Uncaught SyntaxError: Identifier 'x' has already been declared
Hoisting
If we define hoisting as "being able to reference a variable in its scope before the line it is declared, without throwing a Reference Error
(MDN)," var
-declared variables are hoisted, while variables declared with let
and const
are not.
console.log(a) // undefined
var a = 'abc'
console.log(a) // 'abc'
console.log(b); // ReferenceError: can't access lexical declaration 'b' before initialization
let b = 'def'
console.log(c); // ReferenceError: can't access lexical declaration 'c' before initialization
const c = 'ghr'
Execution context, in creation phase, works as if it hoists declaration (but not initialization) of var
-declared variable to the top of scope before executing code. As a result, a
logged undefined
(but not the initialized value abc
) before its declaration, instead of throwing SyntaxError.
let
and const
keywords are considered non-hoisting and any attempt to access them before initialization throws ReferenceError
. Those variables are said to be in a "Temporal Dime Zone (TDZ)," which stretches from the beginning of a scope to the line where variables are declared and initialized with let
or const
.
// beginning of TDZ
console.log(a) // inside TDZ → ReferenceError
let a = 2 // end of TDZ
logA
doesn't throw Syntax Error because even though logA
is declared before a
and thus seems to access a
before declaration, invocation of logA
happens outside TDZ. // beginning of TDZ
const logA = () => console.log(a)
const a = 2 // end of TDZ
logA()
let
and const
really non-hoisting?️It is noteworthy that MDN says that "hoisting is not a term normatively defined in the ECMAScript specification("The spec does define a group of declarations as " HoistableDeclaration," but this only includes
function
, function*
, async function
, and async function*
, as opposed to us often relating the concept with var
keyword) "and outlines how the colloquial usage of the term might apply.• Value Hoisting: Being able to use a variable's value in its scope before the line it is declared (
function
, function
, async function
, and async function*
)• Declaration Hoisting: Being able to reference a variable in its scope before the line it is declared, without throwing a
ReferenceError)
. (var
)• The declaration of the variable causing behavior changes in its scope before the line in which it is declared (
let
, const
, class
declarations; also collectively called lexical declarations)According to the third, broader definition of hoisting, variables declared with
let
or const
keyword also behaves in a way as if they are hoisted.Let's take a look at an example.
While the following code logs 1
,
const x = 1;
if (true) {
console.log(x); // 1
}
the following throws ReferenceError
.
const x = 1;
{
console.log(x); // Uncaught ReferenceError: Cannot access 'x' before initialization
const x = 2;
}
Those who do not agree that let
and const
are non-hoisting argue that
"If theconst x = 2
declaration is not hoisted at all (as in, it only comes into effect when it's executed), then theconsole.log(x)
statement should be able to read thex
value from the upper scope. However, because theconst
declaration sill "taints" the entire scope it's defined in, theconsole.log(x)
statement reads thex
from theconst x = 2
declaration instead (...), and throws aReferenceError
."
While the claim deserves remark, MDN adds to it that it would be still more useful to regard lexical declarations like let
and const
non-hoisting, sayint that "the hoisting of these declarations doesn't bring any meaningful features."
For more informations on hoisting, see also: Hoisting.