Let’s Learn ES2015
The following is a guest post by Ryan Christiani. Ryan is a teacher at HackerYou and has been working on a video series called Let’s Learn ES6. He offered to put some of that together into a tutorial format, which I thought was perfect as we haven’t covered ES2015 much here on CSS-Tricks.
What’s in a name?
As of June 2015, the largest addition to the JavaScript language was finalized. The official name is ECMAScript 2015, sometimes referred to as “ES6”, or now more commonly known as “ES2105”. It is the culmination of years of work and features.
Moving forward, there will be ECMAScript 2016, which will likely be referred to as “ES7” or “ES2016”. The plan is to have incremental yearly releases.
Most browsers have started to implement the ES2015 features, but support varies between them. You can see the current browser compatibility for this implementation using this table.
Tools like Babel allow us to write new ES2015 code today and perform a task called transpiling (much like preprocessing) to convert the code into a earlier version of JavaScript that has greater browser support. This is similar to how Sass works; initially writing your code in Sass syntax, and then a preprocessor compiles to standard CSS.
Overview
In this article we will look at a few features that are now available to use as developers.
We will look at new keywords like let
and const
, how to create template literals to make concatenation easier, the new arrow function syntax, spread operator and rest parameters! Here’s a table of contents:
These additions can help make writing JavaScript a real joy!
let
and const
let
and const
are two new keywords that are now available in ES2015. They are used to declare variables, however there is one key feature these variables share that sets them apart from var
: they create block scoped variables.
When you use the var
keyword to create a variable it is function scoped, and it is local only to that function. This means it is available within the function it was created in and any function nested inside of that one. But it is NOT available outside of there. If you used var
to define a variable outside of any function it would be available globally.
One common issue we will run into with function scoped variables is the for
loop.
for (var i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); // Will print out 10;
It is common to declare a variable inside of the for
loop with the intent of it being bound to just that for
loop however that is not that case. If you run the above code you will see the i
variable is available outside of the for loop.
If you want to use let
or const
you will have to enable strict mode for your JavaScript file first. By adding 'use strict'
at the top of your document you enable a restricted variant of JavaScript.
'use strict';
Strict mode is a way to opt into a version of JavaScript that fixes some mistakes in the language, turning them into errors. It also prohibits syntax that will likely be defined in the future! For example, in strict mode, you cannot make a variable with the name of let
. For more information on strict mode, check out the MDN page on the topic.
A “block” in JavaScript is anything between { }
. So when we talk about block scope, that means that any variables defined in curly brackets will only exist in that block!
var
is function scoped, so creating a variable inside of a block with var
will make it available outside of the block as well.
{
var user = "Ryan";
}
console.log(user); // Ryan
When you define a variable with the let
keyword it will create a new variable only within the { }
or block.
{
let user = "Ryan";
}
console.log(user); // Uncaught ReferenceError: user is not defined
This defines and binds a variable only to the block in which it is in! If we take look at the for
loop example again, and replace var
with let
for (let i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); // Uncaught ReferenceError: i is not defined
Now it works as intended. The const
keyword behaves the exact same way, with one exception. Once the base value is defined, it can never be redefined. It is a read-only value.
const person = 'Ryan';
person = 'Kristen'; // Uncaught TypeError: Assignment to constant variable.
console.log(person);
The browser will throw an error if you try to reassign a value to a variable defined with const
. That being said, you can do something like this.
const person = {
name: 'Ryan'
};
person.name = 'Kristen';
console.log(person); // {name: 'Kristen'}
Using const
does not create an immutable value, the value stored on the person variable is still an object, however we have just changed a property inside of it. If you are looking to lock an object down, look at Object.freeze()
.
When to use let
and when to use const
There is a bit of debate going on right now about when to use let
vs const
. The general rule of thumb is that if you know the value will not be redefined throughout your program, go with const
, if you need a value that might change, go with let
. Letting the browser know that a variable will be constant throughout the program will allow it to make certain adjustments, and this could increase performance!
Template Literals
In ES2015 there is a new way to define a string, and it comes with some added benefits. Currently if you want to define a string, you can use ''
or ""
.
let name = "Ryan";
let job = 'Instructor';
If you want to concatenate strings together you can use the +
operator.
let name = "Ryan";
let job = "Instructor";
let sentence = name + " works at HackerYou as an " + job;
console.log(sentence); // "Ryan works at HackerYou as an Instructor"
As the amount you need to concatenate grows, this pattern gets pretty tedious and unruly. Enter template literals!
To create a template literal string, we use the backtick `
in place of the quotes.
let name = `Ryan`;
let job = `Instructor`;
They behave exactly the same as a regular string literal, but there is one difference. With a template literal, concatenation becomes a lot easier.
let name = `Ryan`;
let job = `Instructor`;
let sentence = `${name} works at HackerYou as an ${job}`;
console.log(sentence); // "Ryan works at HackerYou as an Instructor"
Notice the ${}
syntax inside of the string? This is a template placeholder. It allows us to template out our strings, and the browser will replace the ${}
expression with the proper value at runtime. This makes concatenating large strings a lot more enjoyable.
These new placeholders also allow you to carry out expressions inside!
const price = 9.99;
const shipping = 3.99;
const message = `Your total with shipping will be ${price + shipping}.`;
console.log(message); // Your total with shipping will be 13.98.
Multi line
One last thing to look at with template literals is how they can handle multi line strings. With a regular string if you wanted to have it span more than one line, you would have to do something like this.
const multi = "This is a n multiline string";
console.log(multi);
Including the n
or new line character will force text to go to a new line. If you tried to just put the text on two lines, like this:
const multi = "This is a
multiline string";
console.log(multi);
It would throw an error Uncaught SyntaxError: Unexpected token ILLEGAL
. However with template literals we CAN do just that and add line breaks wherever we’d like!
const multi = `This is a
multiline string`;
console.log(multi);
This allows us to organize our markup in a way that is considerably cleaner!
const person = {
name: 'Ryan',
job: 'Developer/Instructor'
};
const markup = `
<div>
<h2>${person.name}</h2>
<h3>${peron.job}</h3>
</div>
`;
Arrow Functions
Arrow functions are a new syntax for creating functions in ES2015. This does not replace the function() {}
syntax that we know and love, but we will be seeing it more and more as the go-to function syntax.
const add = (a, b) => {
return a + b;
};
The core part of the syntax is the lack of the function
keyword when defining a new function. Instead we have the =>
or fat arrow. You can call the function just as you would any other.
add(2, 3); // 5
There are actually a few ways you can define the arrow function. For example, if the function simply returns a value and there is nothing else in the function body, we can remove the {}
and the return
keyword.
const add = (a, b) => a + b;
The return
here is implicit, meaning that it is implied as opposed to us having to explicitly add return
to our block. If the function only had one parameter you can actually leave the ()
off the definition of the function.
const add5 = a => a + 5;
If there are no parameters to be used in the function, empty parenthesis are used as a placeholder.
const eight = () => 3 + 5;
Or there is a new pattern emerging where people will use a _
as a placeholder in place of the empty parenthesis.
const eight = _ => 3 + 5;
Arrow functions and functional programming
Because the syntax for the arrow function is so small, and most operations in functional programming require very few operations in the function’s body. This syntax is a perfect match for this programming style!
// Without Arrow functions
const numbers = [3,4,5,6,7,8];
const doubleNumbers = numbers.map(function(n) {
return n * 2;
});
// With arrow functions
const numbers = [3,4,5,6,7,8];
const doubleNumbers = numbers.map( n => n * 2 );
The syntax allows you to make this nice and simple operation into one line!
The this
keyword
One place to be cautious of when working with arrow functions is how they handle the this
keyword. Consider a method on an object.
const person = {
firstName: "Ryan",
sayName: function() {
return this.firstName;
}
}
console.log(person.sayName()); // "Ryan"
Inside of the sayName
method, the this
keyword is bound to the person
object. So running the method will produce Ryan
. With an arrow function, the this
keyword is lexically scoped. This means that the scope of the function will be bound based on where it was defined. The value of this
then refers to the parent scope.
const person = {
firstName: "Ryan",
sayName: () => {
return this.firstName;
}
}
console.log(person.sayName()); // undefined
In this example, if we changed the sayName
method from an anonymous function to an arrow function it will return undefined
! The this
will be bound lexically, and in this case it will be the window
object, on which there is no firstName
property. There will be cases where you might want to have that be the correct result! Take a look at this example.
const person = {
firstName: 'Ryan',
hobbies: ['Robots', 'Games', 'Internet'],
showHobbies: function() {
this.hobbies.forEach(function(hobby) {
console.log(`${this.firstName} likes ${hobby}`);
});
}
};
person.showHobbies();
Running this will produce Uncaught TypeError: Cannot read property 'firstName' of undefined
. The this
in the callback function for our .forEach()
method is bound to nothing(in strict mode, in non strict it will be the window
). But if we change the callback to an arrow function we can use the lexically bound this
to get the value we want!
const person = {
firstName: 'Ryan',
hobbies: ['Robots', 'Games', 'Internet'],
showHobbies: function() {
this.hobbies.forEach(hobby => {
console.log(`${this.firstName} likes ${hobby}`);
});
}
};
person.showHobbies();
The this
inside of our forEach
will be bound to the person
object!
Spread Operators
Sometimes we want to do something with an array that we can’t! For example let’s assume we have an array of numbers that we want to find the max of. Math.max seems like the right method for this.
const numbers = [39, 25, 90, 123];
const max = Math.max(numbers);
console.log(max); // NaN
Math.max
is a method that takes a comma separated list of values and will return the highest! Sadly we can not pass an array to it. There is a way to get around this though, we can use a method called .apply
that takes an array and calls a function as if we had passed them in as a list.
const numbers = [39, 25, 90, 123];
const max = Math.max.apply(null, numbers);
console.log(max); // 123
The first argument in .apply
is the value we would like to set the this
value for when we call Math.max
, in this example we provide null
. The second argument is the array we would like to apply to the function. This could be a little confusing, what if there was an easier way to do this?
Enter the Spread Operator
In ES2015 there is the spread operator. The syntax looks like this:
...numbers
What this tool does is spread out, or disperse the elements from the array! It will expand them in place. We can change the above .apply
method call to look something like this now.
const numbers = [39, 25, 90, 123];
const max = Math.max(...numbers);
console.log(max); // 123
Spread will expand the array in place and pass the elements in as if it were a comma separated list.
Using the spread operator to concat
You can also use the spread operator to concatenate arrays together! Since spread expands arrays, we can expand arrays in arrays!
const numbersArray1 = [3, 4, 5, 7, 8];
const numbersArray2 = [9, 6, 10, 11];
const concatArray = [...numbersArray1, ...numbersArray2];
console.log(concatArray); // [3, 4, 5, 7, 8, 9, 6, 10, 11]
Rest Parameters
The spread operator allows us to pass an array of arguments into a function. On the flip side of that, rest parameters allows us to gather the parameters passed to our functions! Just like the spread operator the rest parameter syntax also involves the ...
at the beginning of a variable name.
Let’s look at an example of this. Imagine we have a function that takes any number of arguments and returns the sum, add(2, 3, 4, 5, 6, 7)
would return 27.
const add = function() {
const numbers = Array.prototype.slice.call(arguments);
return numbers.reduce((a,b) => a + b);
};
add(2, 3, 4, 5, 6, 7);
Without rest parameters, we would have to use the arguments
keyword, and call Array.prototype.slice.call(arguments)
. What in the world does Array.prototype.slice.call(arguments)
mean?! arguments
is an Array-LIKE object, meaning it is not an actual array but, is a collection of the arguments passed to a function. However, if we wanted to use an Array method like .reduce()
on arguments
, we would need to do some fiddling.
JavaScript is built up from a bunch of objects. All of these objects have a parent object that they inherit their methods and properties from. They do this via the .prototype
property. Arrays have the .slice
method that we can use to create an actual array from our arguments
value. Using .call
we can call the .slice
method from the prototype with arguments
as the context to create an array….whoa that is a lot.
Enter rest parameters!
const add = function(...numbers) {
return numbers.reduce((a, b) => a + b);
};
add(2, 3, 4, 5, 6, 7);
WOW! That was a lot easier. Rest parameters create an actual array from the arguments passed to a function, so we can use methods like .reduce
on it. This allows us the freedom to perform similar tasks much easier!
It is important to point out that you can mix and match with rest parameters and the spread operator. Consider a function that takes a multiplier as the first argument, and then will multiply any value after it by that number.
const multi = (multiplier, ...numbers) => {
return numbers.map(n => n * multiplier);
}
We define the function with a parameter for the multiplier and use rest parameters to collect however many arguments get passed to this function!
JavaScript moving forward
There are a ton of features in ES2015 that we did not go over here, but hopefully this gives you a good basis of some useful new syntax and additions to the language! If you want to learn more, check out my video series Let’s Learn ES6 on YouTube, as well as letslearnes6.com were you can find out about a book I am writing on ES6.
Let’s Learn ES2015 is a post from CSS-Tricks