What's the difference between let, const and var?

var

Scope

As we all know, there isn’t block-level scope but function-level scope in JavaScript.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
var scope = 'outer scope'
{
var scope = 'inner scope'
console.log(scope) // inner scope
}
console.log(scope) // inner scope
function foo() {
var scope = 'function scope';
console.log(scope) // function scope
}
foo()
console.log(scope) // inner scope

Hoisting

Moreover, declaring a variable at anywhere is equivalent to delcaring it at the top (of the function or global) in JavaScript, which is called hoisting.

For example:

1
2
3
4
5
6
7
console.log(a) // undefined
var a = 1
;(function () {
if (!a) console.log(a) // undefined, since a has been hoisted
var a = 2
console.log(a) // 2
})()

let and const

scope

ECMAScript 2015 (aka ES6) introduced two ways to declare block-level scope local variables: let and const. let‘s function is likes var, and const declares read-only value.

1
2
3
4
5
6
let scope = 'outer scope'
{
let scope = 'inner scope'
console.log(scope) // inner scope
}
console.log(scope) // outer scope

Hoisting

Variables declared by let and const will also be hoisted to the top of the blocks. But unlike var, referencing the variables declared by let and const before their declarations will result in a ReferenceError because of the TDZ (Temporal Dead Zone, the region of a program, where a variable or a parameter cannot be accessed until it’s initialized).

The specification says:

let and const declarations define variables that are scoped to the running execution context’s LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.

Examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let a = 1
{
// ReferenceError
// Because `let a` has been hoisted but cannot be referenced
console.log(a)
let a = 2
}
// equivalent to: let b = undefined, so can be referenced from now on
let b
// ReferenceError
// Assignment happens when the LexicalBinding is evaluated
// However, right-side c is hoisted and under TDZ now...
let c = c
function tdz (x = y, y) {
console.log(x, y)
}
tdz(undefined, 1) // ReferenceError, y is not defined
let q = 1
function tdz2 (p = q, q) {
console.log(p, q)
}
tdz2(undefined, 2) // Still ReferenceError, q is not defined
// Default parameters are evaluated in an intermediate scope
// In which, p = q will resolve q in the intermediate scope,
// but q cannot be referenced until it's initialized

BTW, TDZ is not implemented in most transpilers (like babel, traceur), an issue. :cold_sweat:

References

  1. JavaScript Scoping and Hoisting
  2. var hoisting - MDN
  3. let - MDN
  4. Temporal dead zone (TDZ) demystified
  5. ES6 Notes - Default value of parameters
Comments