Modern JS with ES6+
This is the accompanying blog post to my Modern JS with ES6 talk for the Learn JavaScript Deeply course I taught at WordCamp Miami 2018.
Today in 2018, JavaScript is the most popular language to use on the web, according to the StackOverflow 2018 developer survey.
But the JavaScript we know and love today, is not the same JavaScript from the 2000's or even the early 2010's.
Let's dive in!
ES_???
ES is short for ECMAScript. Every time you see ES followed by a number, it is referencing that version of ECMAScript.
Only recently, with ES6 or ES2015 did JavaScript make the biggest leap in syntax and added functionality. These additions were intended to make large-scale JavaScript development easier.
- ES1: June 1997
- ES2: June 1998
- ES3: December 1999
- ES4: Abandoned
- ES5: December 2009
- ES6/ES2015: June 2015
- ES7/ES2016: June 2016
- ES8/ES2017: June 2017
- ES9/ES2018: June 2018
- ES10/ES2019: June 2019
- ES11/ES2020: June 2020
- ES.Next: This term is dynamic and references the next version of ECMAScript coming out.
TC39
The TC39 is the group that is responsible for advancing the ECMAScript specifications and standardizing these specifications for the JavaScript language.
All changes to the specification are developed within a process that evolves a feature from the idea phase to a fully fledged and specified feature.
- Stage 0 - Strawperson
- Stage 1 - Proposal
- Stage 2 - Draft
- Stage 3 - Candidate
- Stage 4 - Finished
You can see an active list of proposals for the TC39, here.
Modules
Modules allow you to load code asynchronously and provides a layer of abstraction to your code. While JavaScript has had modules for a long time, they were previously implemented with libraries and not built into the language itself. ES6 is the when the JavaScript language first had built-in modules.
There are two ways to export from a module.
- Named exports
- Default export
Named Exports
With named exports, you just prepend what you want to export with export
.
And then they can be imported using the same name as the object you exported.
Default Export
With default exports, you just prepend what you want to export with export default
.
Default and Named Exports
The two ways can even be mixed!
Variable Scoping
var
vs let
vs const
var
is either function-scoped or globally-scoped, depending on the context.
If a var
is defined within a function, it will be scoped to that enclosing function as well as any functions declared within. And would be globally scoped if it is declared outside of any function.
let
and const
are block scoped.
You can create new block scopes with curly brackets {}
as shown in the below code sample.
let
and const
const
variables can only be assigned once. But it is NOT immutable.
But, you can change the properties!
Object.freeze()
prevents changing the properties. Freezing an object returns the originally passed in object and has a multitude of effects on the object.
It will prevent new properties from being added to it, existing properties from being removed, changing the value of properties, and even prevents the prototype from being changed as well.
The object's properties are made immutable with Object.freeze()
Another option you have is to seal an object to prevent changing the object structure.
Wait, no. Not that Seal!
Yeah, no. Not that one either...
Hoisting
Hoisting in JavaScript is a process where variables and function declarations are moved to the top of their scope before code execution.
Which means you can do this with functions and vars:
In ES6, classes, let
, and const
variables are hoisted but they are not initialized yet unlike var
variables and functions.
Temporal Dead Zone
While the temporal dead zone sounds like something from the plot of a Doctor Who episode, it is way less scary than it sounds.
A const
or let
variable is in a temporal dead zone from the start of the block until the initialization is processed.
Referencing the variable in the block before the initialization results in a ReferenceError
, contrary to a variable declared with var
, which will just have the undefined
value and type.
var
? let
? const
?
But, what should I use?!? The only difference between const
and let
is that const
makes the contract that no rebinding will happen.
Use
Mathias Bynens - V8 Engineer @ Googleconst
by default. Only uselet
if rebinding is needed.var
shouldn't be used in ES2015.
Use
Kyle Simpson - Founder @ Getify Solutionsvar
for top level variables Uselet
for localized variables in smaller scopes. Refactorlet
toconst
only after some code has been written and you're reasonably sure there shouldn't be variable reassignment.
I, personally, follow the first approach of using const
by default, never use var
, and only use let
when I know I need to reassign the variable.
Iterables & Looping
When iterating or looping using var
, you leak a global variable to the parent scope and the variable gets overwritten with every iteration.
Using let
in a for
loop allows us to have the variable scoped to its block only.
ES6 also gives us a new way to loop over iterables!
Using a for...of
statement loops over an iterable with a very clean and simple syntax.
You can even iterate over NodeLists
without having to use any other trickery! 🤯
Or even, use it to iterate over letters in a string!
Arrow Functions
Arrow functions are a more concise alternative to the traditional function expression.
Arrow functions can have implicit returns:
this
?
What about With the introduction of ES6, the value of this
is picked up from its surroundings or the function's enclosing lexical context. Therefore, you don't need bind()
, that
, or self
anymore!
When should I not use arrow functions?
Check out my article, about when to not use arrow functions in JavaScript
Default Arguments
Here's a basic function.
Let’s add some defaults to the arguments in our function expression!
The old way 😕
A little better
Now with ES6+! 🎉🎉🎉
What if I wanted to only pass in the first and third argument?
Destructuring
Destructuring allows you to extract multiple values from any data that is stored in an object or an array.
Using the above object, let's create some variables from the object's properties.
Before ES6, we were stuck just initializing a variable and assigning it to the object property you'd like to extract.
But with ES6+, we're able to destructure the variable and create new variables from the extracted data.
You can also alias the extracted data to a different variable name!
It even works with nested properties!
What if I tried to destruct a property that doesn't exist?
The returned value for that variable will be undefined
.
You can even set defaults in your destructuring!
You can destructure arrays as well!
Spread... and ...Rest
Before ES6, we would run .apply()
to pass in an array of arguments.
...Spread Operator
But with ES6, we can use the spread operator ...
to pass in the arguments.
We can also use the spread operator to combine arrays.
And you can combine them at any point in the array!
We can also use the spread operator to create a copy of an array.
We can also use the spread operator with destructuring.
We can also use the spread operator to expand a NodeList.
...Rest Operator
The rest operator allows us to more easily handle a variable number of function parameters.
Template Literals
The template literal, introduced in ES6, is a new way to create a string.
Within template literals you can evaluate expressions.
With template literals you can more easily create multi-line strings.
New String Methods
With ES6, we have a few new string methods that can be very useful.
.startsWith()
This method returns a bool of whether the string begins with a substring, starting at the index provided as the second argument, which defaults to 0
.
Syntax
Examples
.endsWith()
This method returns a bool of whether the string ends with a substring, with the optional parameter of length which is used as the length of the str
and defaults to str.length
.
Syntax
Examples
.includes()
This method returns a bool of whether the string includes a substring, starting at the index provided as the second argument, which defaults to 0
.
Syntax
Examples
.repeat()
This method returns a new string which contains a concatenation of the specified number of copies of the original string.
Syntax
Examples
Enhanced Object Literals
Let's assign our variables to properties of an object!
Now let's do it again but with object literals this time.
You can even mix object literals with normal key value pairs.
We can also use a shorter syntax for method definitions on objects initializers.
The syntax we're all familiar with in ES5...
Can now be simplified with the new syntax for method definitions!
Or even define keys that evaluate on run time inside object literals.
Let's clean that up a bit with string template literals for the keys!
New Array Methods!
Array.find()
The Array.find
static method returns the first value in the provided array that satisfy the testing function or undefined if no elements match.
Syntax
Examples
Array.findIndex()
The Array.findIndex
static method returns the index of the first value in the provided array that satisfy the testing function or undefined if no elements match.
Syntax
Examples
Array.from()
The Array.from
static method let's you create Array
s from array-like objects and iterable objects.
Syntax
Examples
We all know that unfortunately we cannot just simply loop over a NodeList.
But, using Array.from
we can loop over them easily!
Array.indexOf()
The Array.indexOf
static method returns the index at which passed in element can be found in an array.
Syntax
Examples
Array.of()
The Array.of
static method creates a new Array
that consists of the values that are passed into it regardless of the type of or the number of arguments.
The big difference between this and the Array
constructor method is how each handles single integer arguments.
Array.of(5)
will create an array with a single element but Array(5)
creates an empty array with a length of 5.
Syntax
Examples
Promises
The Promise object is used for asynchronous operations and represents the eventual completion or failure of that operation, and its resulting value. 1 Promises are often used for fetching data asynchronously.
Promises exist in one of four states:
- Pending - When defined, this is the initial state of a Promise.
- Fulfilled - When the operation has completed successfully.
- Rejected - When the operation has failed.
- Settled - When it has either fulfilled or rejected.
Promise API
There are 6 static methods in the Promise class:
Promise.all
The .all()
method accepts an iterable of Promise
objects as it's only parameter and returns a single Promise
object that resolves to an array of the results of the Promise
objects that were passed into it.
The promise returned from this method will resolve when all of the passed in promises resolve or will reject, if any of them rejected.
Promise.allSettled
The .allSettled()
method accepts an iterable of Promise
objects as it's only parameter and will return a Promise
object that will resolve with an array of objects with the outcome of each Promise
object when all of them have settled with either a resolve or reject.
The biggest difference between .allSettled()
and .all()
is that the later resolves or rejects based on the outcomes of the passed in promises while the former will resolve once all Promise
objects passed in are settled with either a resolve
or reject
.
Promise.any
The .any()
method accepts an iterable of Promise
objects as it's only parameter and will return a single Promise
objet that resolves when any of the passed in Promise
objects resolve, with the value of the resolved original Promise
object.
Promise.race
The .race()
method accepts an iterable of Promise
objects as it's only parameter and will return a Promise
object that resolves or rejects as soon as one of the passed in Promise
objects resolves or rejects.
Promise.resolve
The .resolve()
method returns a Promise
object that is resolved with the value provided.
If a thenable
(has a then
method) is the returned value, the returned Promise
object will follow that then
until the final value is returned.
Promise.reject
The .reject()
method returns a Promise
object that is rejected with the reason provided.
Let's jump into some examples!
Promises can even be chained!
Let's look at some more in depth examples.
Often, you will run into using promises when trying to fetch data.
You can catch errors that are thrown in promises using the .catch
method as shown below.
Classes
Behind the scenes, ES6 classes are not something that is radically new. They mainly provide more convenient syntax to create old-school constructor functions.
Async/Await
The await
keyword and async
functions were added in ES2017 or ES8.
They are really just syntactic sugar on top of Promises that help make asynchronous code easier and cleaner to read and write.
The async
keyword is added to functions to denote it will return a promise to be resolved, rather than directly returning the value.
Currently, you can only use the await
keyword inside of an async
function, but there is an open proposal for top-level await.
Let's take a look at some promises and how we can convert them to async/await!
And now, let's turn change this to use async/await.
The difference being, we're now wrapping the entire function body where we do our await
s in a try/catch
block. This will catch any errors from the Promises that we are awaiting.
We are also putting the async
keyword in front of our function definition here as well, which is required for await
to work properly.
Set/WeakSet
A Set object allows you to store unique values of any type. While it is similar to an Array, there are a couple of big differences. The first being that an Array can have duplicate values and a Set cannot. And the other is that Arrays are ordered by index while Sets are only iterable in the order they were inserted.
You can capture the entries in the set using .entries()
which will keep the reference to the original objects inserted.
Map and Set both have keys()
and values()
methods but on Sets, those methods will return the same iterable of values since Sets are not key value pairs but just values.
The main difference between a Set and a WeakSet is that the latter only accepts objects and the former can accept values of any type.
The other big difference is that WeakSets hold weak references to the objects within them.
So if there are no further references to an object that is within a WeakSet, those objects can be garbage collected.
WeakSet's are also not enumerable nor is there a way to list the current objects within it, but you can .add()
, .delete()
, and check if a WeakSet .has()
an object.
If you were to use the above code sample and then force garbage collection in the Chrome dev-tools by clicking the trash can under the Performance tab, you'd see that ws
no longer holds the reference to user
.
Map/WeakMap
The Map object, on the other hand, hold key-value pairs, tracks the insertion order, and can use either objects or primitive values for both the key and value.
While Objects are very similar to Maps and historically have been used in place of Maps prior to their introduction in ES6, there are some key differences.
Maps can have keys of any type of value, including functions and Objects, while Objects can only have Strings and Symbols as keys.
You cannot directly iterate over an Object while there are several ways to iterate over a Map.
Also, there is no native support for JSON.stringify()
, but it is possible with a bit of work.
References
Last updated: