Intro to ES6 - let and const
We're starting a series on ECMAScript 6 2015 (ES6). You might be thinking, "but it's 2017!" True, some people have fully adopted the new standards long ago, but many haven't and many that have may not be using it to its full potential. It's also one of the largest incremental changes to JavaScript, meaning there are a lot of new features to learn. Now that Node and browsers have provided near full support, it's time get on board and up your JS game.
ECMAScript is the standard for defining the behavior of JavaScript. It is important to note that JavaScript is an implementation of that standard, and browsers and platforms like Node JS provide their own superset of that. This means that not all implementations are feature aligned, so it is important to understand which features of ECMAScript are available in the implementation of JS you are using.
Lesson 1: Block Bindings - let
and const
Bindings are essentially just variables, but have been a source of confusion due to how JS hoists things. If you need a refresher on hoisting, here ya go.
Since most other languages create the variable where the declaration occurs, changes were made to make the behavior more intuitive. The keywords let
and const
were added as a way to declare a variable. Previously, var
was the only way. var
does not have block scope, only function scope. For a refresher on scope, go here.
Since you're likely quite familiar with var
, let's focus on let
and const
, which have block scope. Block scope is anything inside curly brackets {}
, including things like if
, and else
. Block scope also applies to variables declared within a function, and follow lexical scoping rules.
Previously variables only had functional scope and would always be hoisted to the top of the containing scope.
Problem
function greet(condition) {
if (condition) {
var greeting = 'hello'
return greeting
} else {
// greeting exists here as undefined
return 'good bye'
}
// greeting exists here as undefined
}
What JS is doing
function greet(condition) {
var greeting
if (condition) {
greeting = 'hello'
return greeting
} else {
return 'good bye'
}
}
That is quite confusing to most people used to working in other languages. JavaScript is hoisting the variable declaration to the top of the scope, which makes it undefined everywhere, except for the if
block where the value hello
is assigned. This is a source of errors.
In come let
and const
...
let declarations
let
and var
declarations are almost identical, the biggest difference being that let
declarations are only hoisted to the top of a block, rather than the entire function. This means you should declare your let
variables at the top of the block to avoid the temporal dead zone. More on that in a bit. Let's see how that affects our previous example.
function greet(condition) {
if (condition) {
let greeting = 'hello'
return greeting
} else {
// greeting does not exist here
return 'good bye'
}
// greeting does not exist here
}
One more thing to note. **`let` will not redeclare a variable already declared in the same scope**
You can't do this:
var number = 42
// TypeError: Identifier 'number' has already been declared
let number = 500
But you can do this:
var number = 42
if (number === 42) {
// this works because it is a separate scope
// and thus creates a different variable called number
let number = 500
console.log(number) // 500
}
console.log(number) // 42
###### const declarations
const
is used to define constants, meaning they cannot be changed once set. This also means that they must be initialized immediately, or a syntax error will be thrown.
// SyntaxError: Missing initializer in const declaration
const ID
const
behaves the same as let
, in that it will throw an error when trying to redeclare a variable of the same name on the same scope, and that it will not hoist declarations and keep the scope within the block.
const ID = 1
// TypeError: Assignment to constant variable.
ID = 2
The one caveat to all this is that you can modify a const
variable if it is an object. A const
declaration prevents modification of the binding, not of the value.
const people = {
teacher: "Mike",
student: "Seth"
}
// this is OK
people.assistant = "Alice"
people.teacher = "Alice"
// TypeError: Assignment to constant variable.
people = {
name: "Brad"
}
Remember the **Temporal Dead Zone**?
This refers to the zone between the top of the scope and where a let
or const
is declared, since these values cannot be accessed before their declaration. Doing so will throw a reference error. Let's see two examples of this.
The following will throw an error because it tries to access a variable before the statement gets executed. The subtlety here, is that the JavaScript engine knows of it's existence in the scope, so it errors when trying to access it. The following behavior applies to both let
and const
.
if (condition) {
// temporal dead zone
// ReferenceError: greeting is not defined
console.log(greeting)
let greeting = 'hello'
}
If we look at the typeof
value for greeting
in a different scope, it just thinks the variable hasn't been defined.
console.log(typeof greeting) // undefined
if (true) {
let greeting = 'hello'
}
**Loops**
Loops are a foundation of programming, and let
especially brings some improvement. Previously, using functions in loops was an issue, due to the loop variable being available outside of the loop, sometimes forcing developers to use immediately invoked function expressions (IIFE) to create a containing scope.
var fns = []
for (var i = 0; i < 10; i++) {
fns.push(function() {
console.log(i)
})
}
for (var j = 0; j < fns.length; j++) {
var fn = fns[j]
fn() // outputs the number 10, ten times.
}
The code above does not perform as intended, as instead of outputting the value of the ith
iteration, it just logs the last value, 10
. This is because that value is scoped outside of the loop. We can fix this by using an IIFE, since IIFEs create their own scope.
var fns = []
for (var i = 0; i < 10; i++) {
fns.push((function(i) {
return function() {
console.log(i)
}
}(i)))
}
for (var j = 0; j < fns.length; j++) {
var fn = fns[j]
fn() // outputs 0, 1, 2, ... 9
}
With let
and const
we can avoid this. On each iteration let
will create a new variable and initialize it to the value of the variable with the same name of the previous iteration, meaning each function will get its own copy of i
. If we change var
to let
in our original example, we will get the desired behavior, without having to use an IIFE.
let fns = []
for (let i = 0; i < 10; i++) {
fns.push(function() {
console.log(i)
})
}
for (let j = 0; j < fns.length; j++) {
let fn = fns[j]
fn() // outputs 0, 1, 2, ... 9
}
The same behavior applies to for ... in
and for ... of
loops. const
can be a bit tricky, since its value cannot be changed. What this ultimately means is that you shouldn't use it in a for
loop. It will error in a for
loop, but it will work in a for ... of
and for ... in
, so long as you don't change the value in each iteration.
Global Block Bindings
One more important differentiation from var
is that in let
and const
a new binding is created in the global scope, but the property is not added to the global object. This is different from how var
works, where it assigns properties to the global object, often overwriting property values without intent.
Using var
// in a browser
var RegExp = 'my string'
console.log(window.RegExp) // "my string"
var str = 'another string'
console.log(window.str) // "another string"
Using let
// in a browser
let RegExp = 'my string'
console.log(RegExp) // "my string"
console.log(window.RegExp === RegExp) // false
const str = 'another string'
console.log(str) // "another string"
console.log("str" in window) // false
You still might have a use case to use var
if you need to write something to the global object. This would be common in a browser where things need to be shared across frames and windows.
Best Practices
You're probably left thinking, "Which should I use now, var
, let
, or const
?" Use const
by default and let
when the value needs to change. Leave var
for special cases.