Modern JS with ES6+
Kevin Langley Jr
Posted on December 17, 2021
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
.
// mathlib.js
export function square(x) {
return x * x;
}
export function add(x, y) {
return x + y;
}
And then they can be imported using the same name as the object you exported.
// main.js
import { square, add } from './mathlib';
console.log(square(9)); // 81
console.log(add(4, 3)); // 7
Default Export
With default exports, you just prepend what you want to export with export default
.
// foo.js
export default () => {
console.log('Foo!');
}
// main.js
import foo from './foo';
foo(); // Foo!
Default and Named Exports
The two ways can even be mixed!
// foobar.js
export default () => {
console.log('Foo!');
}
export const bar() => {
console.log( 'Bar!' )
}
// main.js
import foo, { bar } from './foobar';
foo(); // Foo!
bar(); // Bar!
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.
if ( true ) {
var foo = 'bar';
}
console.log( foo ); // bar
function hello() {
var x = 'hello';
function world() {
var y = 'world';
console.log(x); // hello (function `hello()` encloses `x`)
console.log(y); // world (`y` is in scope)
}
world();
console.log(x); // hello (`x` is in scope)
console.log(y); // ReferenceError `y` is scoped to `world()`
}
hello();
let
and const
are block scoped.
if ( true ) {
let foo = 'bar';
const bar = 'foo';
}
console.log( foo ); // ReferenceError.
console.log( bar ); // ReferenceError.
You can create new block scopes with curly brackets {}
as shown in the below code sample.
let
and const
let first = 'First string';
{ // Each layer of curly brackets gives us a new block scope.
let second = 'Second string';
{
let third = 'Third string';
}
// Accessing third here would throw a ReferenceError.
}
// Accessing second here would throw a ReferenceError.
const
variables can only be assigned once. But it is NOT immutable.
const foo = { bar: 1 };
foo = 'bar'; // 'foo' is read only.
But, you can change the properties!
const foo = { bar: 1 };
foo.bar = 2;
console.log(foo); // { bar: 2 }
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()
const foo = { bar: 1 };
Object.freeze(foo);
foo.bar = 3; // Will silently fail or in strict mode, will return a TypeError
console.log(foo.bar); // 2
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...
const foo = { bar: 1 };
Object.seal(foo);
foo.baz = false; // Will silently fail or in strict mode, will return a TypeError
foo.bar = 2;
console.log( foo ); // { bar: 2 }
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:
sayHello();
function sayHello() {
console.log('Hello!');
}
console.log( foobar ); // undefined (note: not ReferenceError!)
var foobar = 'Woot!'
In ES6, classes, let
, and const
variables are hoisted but they are not initialized yet unlike var
variables and functions.
new Thing(); // TypeError
class Thing{};
console.log(foo); // 'foo' was used before it was defined
let foo = true;
console.log(bar); // 'bar' was used before it was defined
const bar = true;
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.
if ( true ) {
// TDZ starts!
const doSomething = function () {
console.log( thing ); // OK!
};
doSomething(); // ReferenceError
let thing = 'test'; // TDZ ends.
doSomething(); // Called outside TDZ!
}
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.
But, what should I use?!? var
? let
? const
?
The only difference between const
and let
is that const
makes the contract that no rebinding will happen.
Use
const
by default. Only uselet
if rebinding is needed.var
shouldn't be used in ES2015.Mathias Bynens - V8 Engineer @ Google
Use
var
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.Kyle Simpson - Founder @ Getify Solutions
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.
for ( var i = 0; i < 10; i++ ) {
setTimeout( function() {
console.log( 'Number: ' + i );
}, 1000 );
}
// Number: 10
// Number: 10
// Number: 10
// Number: 10
// Number: 10
// Number: 10
// Number: 10
// Number: 10
// Number: 10
// Number: 10
Using let
in a for
loop allows us to have the variable scoped to its block only.
for ( let i = 0; i < 10; i++ ) {
setTimeout( function() {
console.log( 'Number: ' + i );
}, 1000 );
}
// Number: 0
// Number: 1
// Number: 2
// Number: 3
// Number: 4
// Number: 5
// Number: 6
// Number: 7
// Number: 8
// Number: 9
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.
const iterable = [10, 20, 30];
for (const value of iterable) {
console.log(value);
}
// 10
// 20
// 30
You can even iterate over NodeLists
without having to use any other trickery! 🤯
const articleParagraphs = document.querySelectorAll('article > p');
for (const paragraph of articleParagraphs) {
paragraph.classList.add('read');
}
Or even, use it to iterate over letters in a string!
const foo = 'bar';
for (const letter of foo) {
console.log(letter);
}
// b
// a
// r
Arrow Functions
Arrow functions are a more concise alternative to the traditional function expression.
// Traditional function expression.
const addNumbers = function (num1, num2) {
return num1 + num2;
}
// Arrow function expression.
const addNumbers = (num1, num2) => {
return num1 + num2;
}
Arrow functions can have implicit returns:
// Arrow function with implicit return and without any arguments.
const sayHello = () => console.log( 'Hello!' );
// Arrow function with implicit return and a single argument.
const sayHello = name => console.log( `Hello ${name}!` );
// Arrow function with implicit return and multiple arguments.
const sayHello = (firstName, lastName) => console.log( `Hello ${firstName} ${lastName}!` );
What about this
?
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!
function Person(){
this.age = 0;
setInterval(function() {
this.age++; // `this`refers to the Window 😒
}, 1000);
}
function Person(){
var that = this;
this.age = 0;
setInterval(function() {
that.age++; // Without arrow functions. Works, but is not ideal.
}, 1000);
}
function Person(){
this.age = 0;
setInterval(() => {
this.age++; // `this` properly refers to the person object. 🎉🎉🎉
}, 1000);
}
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.
function calculateTotal( subtotal, tax, shipping ) {
return subtotal + shipping + (subtotal * tax);
}
const total = calculateTotal(100, 0.07, 10);
Let’s add some defaults to the arguments in our function expression!
The old way 😕
function calculateTotal( subtotal, tax, shipping ) {
if ( tax === undefined ) {
tax = 0.07;
}
if ( shipping === undefined ) {
shipping = 10;
}
return subtotal + shipping + (subtotal * tax);
}
const total = calculateTotal(100);
A little better
function calculateTotal( subtotal, tax, shipping ) {
tax = tax || 0.07;
shipping = shipping || 10;
return subtotal + shipping + (subtotal * tax);
}
const total = calculateTotal(100);
Now with ES6+! 🎉🎉🎉
function calculateTotal( subtotal, tax = 0.07, shipping = 10 ) {
return subtotal + shipping + (subtotal * tax);
}
const total = calculateTotal(100);
What if I wanted to only pass in the first and third argument?
function calculateTotal( subtotal, tax = 0.07, shipping = 10 ) {
return subtotal + shipping + (subtotal * tax);
}
const total = calculateTotal(100, , 20); // SyntaxError! Cannot pass empty spaces for argument.
const total = calculateTotal(100, undefined, 20); // 🎉🎉🎉🎉
Destructuring
Destructuring allows you to extract multiple values from any data that is stored in an object or an array.
const person ={
first: 'Kevin',
last: 'Langley',
location: {
city: 'Beverly Hills',
state: 'Florida'
}
};
Using the above object, let's create some variables from the object's properties.
const first = person.first;
const last = person.last;
Before ES6, we were stuck just initializing a variable and assigning it to the object property you'd like to extract.
const { first, last, location } = person;
const { city, state } = location;
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!
const { first: fName, last: lName } = person;
const { city: locationCity, state: locationState } = person.location;
It even works with nested properties!
const { first, last, location: { city } } = person;
console.log( city ); // Beverly Hills
What if I tried to destruct a property that doesn't exist?
The returned value for that variable will be undefined
.
const settings = { color: 'white', height: 500 };
const { width, height, color } = settings;
console.log(width);// undefined
console.log(height); // 500
console.log(color);// white
You can even set defaults in your destructuring!
const settings = { color: 'white', height: 500 };
const { width = 200, height = 200, color = 'black' } = settings;
console.log(width);// 200
console.log(height); // 500
console.log(color);// white
You can destructure arrays as well!
const details = ['Kevin', 'Langley', 'kevinlangleyjr.com'];
const [first, last, website] = details;
console.log(first);// Kevin
console.log(last);// Langley
console.log(website); // kevinlangleyjr.com
Spread... and ...Rest
Before ES6, we would run .apply()
to pass in an array of arguments.
function doSomething (x, y, z) {
console.log(x, y, z);
}
let args = [0, 1, 2];
// Call the function, passing args.
doSomething.apply(null, args);
...Spread Operator
But with ES6, we can use the spread operator ...
to pass in the arguments.
function doSomething (x, y, z) {
console.log(x, y, z);
}
let args = [0, 1, 2];
// Call the function, without `apply`, passing args with the spread operator!
doSomething(...args);
We can also use the spread operator to combine arrays.
let array1 = ['one', 'two', 'three'];
let array2 = ['four', 'five'];
array1.push(...array2) // Adds array2 items to end of array.
array1.unshift(...array2) //Adds array2 items to beginning of array.
And you can combine them at any point in the array!
let array1 = ['two', 'three'];
let array2 = ['one', ...array1, 'four', 'five'];
console.log(array2); // ["one", "two", "three", "four", "five"]
We can also use the spread operator to create a copy of an array.
let array1 = [1,2,3];
let array2 = [...array1]; // like array1.slice()
array2.push(4);
console.log(array1); // [1,2,3]
console.log(array2); // [1,2,3,4]
We can also use the spread operator with destructuring.
const players = ['Kevin', 'Bobby', 'Nicole', 'Naomi', 'Jim', 'Sherry'];
const [first, second, third, ...unplaced] = players;
console.log(first); // Kevin
console.log(second); // Bobby
console.log(third); // Nicole
console.log(unplaced); // ["Naomi", "Jim", "Sherry"]
const { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x); // 1
console.log(y); // 2
console.log(z); // { a: 3, b: 4 }
We can also use the spread operator to expand a NodeList.
const elements = [...document.querySelectorAll('div')];
console.log(elements); // Lists all the div's on the page.
...Rest Operator
The rest operator allows us to more easily handle a variable number of function parameters.
function doMath(operator, ...numbers) {
console.log(operator); // 'add'
console.log(numbers); // [1, 2, 3]
}
doMath('add', 1, 2, 3);
Template Literals
The template literal, introduced in ES6, is a new way to create a string.
const name = 'Kevin';
// The old way...
console.log('Hello, ' + name + '!'); // Hello, Kevin!
const name = 'Kevin';
// With ES6 template literals.
console.log(`Hello, ${name}!`); // Hello, Kevin!
Within template literals you can evaluate expressions.
const price = 19.99;
const tax = 0.07;
const total = `The total price is ${price + (price * tax)}`;
console.log(total); // The total price is 21.3893
With template literals you can more easily create multi-line strings.
console.log('This is some text that flows across\ntwo lines!');
// This is some text that flows across
// two lines!
console.log(`But so does
this text!`);
// But so does
// this text!
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
startsWith( searchString )
startsWith( searchString, position )
Examples
const str = 'Learn JavaScript Deeply';
console.log(str.startsWith('Learn'));// true
console.log(str.startsWith('JavaScript'));// false
console.log(str.startsWith('Deeply', 17));// true
.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
endsWith( searchString )
endsWith( searchString, length )
Examples
const str = 'Learn JavaScript Deeply';
console.log(str.endsWith('Deeply'));// true
console.log(str.endsWith('Learn'));// false
console.log(str.endsWith('JavaScript', 16));// true
.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
includes( searchString )
includes( searchString, position )
Examples
const str = 'Learn JavaScript Deeply';
console.log(str.includes('JavaScript'));// true
console.log(str.includes('Javascript'));// false
console.log(str.includes('PHP'));// false
.repeat()
This method returns a new string which contains a concatenation of the specified number of copies of the original string.
Syntax
repeat( count )
Examples
const str = 'Deeply';
console.log(str.repeat(3));// DeeplyDeeplyDeeply
console.log(str.repeat(2.5));// DeeplyDeeply (converts to int)
console.log(str.repeat(-1));// RangeError
Enhanced Object Literals
const first = 'Kevin';
const last = 'Langley';
const age = 29;
Let's assign our variables to properties of an object!
const person = {
first: first,
last: last,
age: age
};
Now let's do it again but with object literals this time.
const person = {
first,
last,
age
};
You can even mix object literals with normal key value pairs.
const person = {
firstName: first,
lastName: last,
age
};
We can also use a shorter syntax for method definitions on objects initializers.
The syntax we're all familiar with in ES5...
var obj = {
foo: function() {
console.log('foo');
},
bar: function() {
console.log('bar');
}
};
Can now be simplified with the new syntax for method definitions!
const obj = {
foo() {
console.log('foo');
},
bar() {
console.log('bar');
}
};
Or even define keys that evaluate on run time inside object literals.
let i = 0;
const a = {
['foo' + ++i]: i,
['foo' + ++i]: i,
['foo' + ++i]: i
};
console.log(a.foo1); // 1
console.log(a.foo2); // 2
console.log(a.foo3); // 3
Let's clean that up a bit with string template literals for the keys!
let i = 0;
const a = {
[`foo${++i}`]: i,
[`foo${++i}`]: i,
[`foo${++i}`]: i
};
console.log(a.foo1); // 1
console.log(a.foo2); // 2
console.log(a.foo3); // 3
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
// Arrow function
Array.find( element => { conditional } )
Array.find((element, index) => { conditional } )
Array.find((element, index, array) => { conditional } )
// Callback function
Array.find(callbackFn)
Array.find(callbackFn, thisArg)
// Inline callback function
Array.find(function callbackFn(element) { conditional })
Array.find(function callbackFn(element, index) { conditional })
Array.find(function callbackFn(element, index, array){ conditional })
Array.find(function callbackFn(element, index, array) { conditional }, thisArg)
Examples
const posts = [
{
id: 1,
title: 'Hello World!'
},
{
id: 2,
title: 'Learn JS Deeply!'
}
];
const post = posts.find(post => post.id === 2);
console.log(post); // {id: 2, title: "Learn JS Deeply!"}
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
// Arrow function
Array.findIndex( element => { conditional } )
Array.findIndex((element, index) => { conditional } )
Array.findIndex((element, index, array) => { conditional } )
// Callback function
Array.findIndex(callbackFn)
Array.findIndex(callbackFn, thisArg)
// Inline callback function
Array.findIndex(function callbackFn(element) { conditional })
Array.findIndex(function callbackFn(element, index) { conditional })
Array.findIndex(function callbackFn(element, index, array){ conditional })
Array.findIndex(function callbackFn(element, index, array) { conditional }, thisArg)
Examples
const posts = [
{
id: 1,
title: 'Hello World!'
},
{
id: 2,
title: 'Learn JS Deeply!'
}
];
const post = posts.findIndex(post => post.id === 2);
console.log(post); // 1 - Remember, this is zero based!
Array.from()
The Array.from
static method let's you create Array
s from array-like objects and iterable objects.
Syntax
// Simple without any mapping
Array.from(arrayLike);
// Arrow function mapping (Can't utilize the third argument for the `thisArg` argument since the mapping uses an arrow function)
Array.from(arrayLike, element => [...] );
Array.from(arrayLike, (element, index) => [...] );
// Mapping named function
Array.from(arrayLike, mapFn );
Array.from(arrayLike, mapFn, thisArg);
Examples
We all know that unfortunately we cannot just simply loop over a NodeList.
const headers = document.querySelectorAll('h1');
const titles = headers.map(h1 => h1.textContent); // TypeError: headers.map is not a function
But, using Array.from
we can loop over them easily!
const headers = document.querySelectorAll('h1');
const headersArray = Array.from(headers);
const titles = headersArray.map(h1 => h1.textContent);
// Or we can use the `mapFn` parameter of `Array.from`.
const titles = Array.from(
document.querySelectorAll('h1'),
h1 => h1.textContent
);
Array.indexOf()
The Array.indexOf
static method returns the index at which passed in element can be found in an array.
Syntax
Array.indexOf(searchElement)
Array.indexOf(searchElement, index)
Examples
const values = Array.of(123, 456, 789);
console.log(values); // [123,456,789]
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
Array.of(firstElement)
Array.of(firstElement, secondElement, ... , nthElement)
Examples
const values = Array.of(123, 456, 789);
console.log(values); // [123,456,789]
Promises
The Promise object is used for asynchronous operations and represents the eventual completion or failure of that operation, and its resulting value. 1Promises 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.
const promiseA = new Promise( ( resolve, reject ) => {
setTimeout( () => {
resolve('Resolved!');
}, 300 );
} );
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!
const p1 = new Promise( (resolve, reject) => {
resolve('Learn JavaScript Deeply!');
} );
p1.then(data => console.log(data)); // Learn JavaScript Deeply
Promises can even be chained!
const p2 = new Promise( (resolve, reject) => {
setTimeout( () => resolve( 1 ), 1000 ); // Resolve with `1` after 1 second.
} );
p2
.then( value => {
console.log( value ); // 1
return value * 2;
} )
.then( value => {
console.log( value ); // 2
return value * 4;
} )
.then( value => {
console.log( value ); // 8
return value * 2;
} );
Let's look at some more in depth examples.
Often, you will run into using promises when trying to fetch data.
const postsPromise = fetch('https://miami.wordcamp.org/2018/wp-json/wp/v2/posts');
console.log(postsPromise);
// Promise {pending}
// __proto__: Promise
// [[PromiseStatus]]: "pending"
// [[PromiseValue]]: undefined
postsPromise
.then(data => console.log(data));
// Response {type: "cors", url: "https://miami.wordcamp.org/2018/wp-json/wp/v2/posts", redirected: false, status: 200, ok: true, ...}
postsPromise
.then(data => data.json()) // Get's JSON value from fetch.
.then(data => console.log(data)) // Console log the value.
.catch(err => console.error(err)); // Catch any errors.
// {id: 5060, date: "2018-03-15T17:41:09", ...}
// {id: 4954, date: "2018-03-14T00:21:10", ...}
// {id: 4943, date: "2018-03-13T19:16:11", ...}
// {id: 4702, date: "2018-03-10T11:04:36", ...}
// ...
You can catch errors that are thrown in promises using the .catch
method as shown below.
const p = new Promise((resolve, reject) => {
reject(Error('Uh oh!'));
});
p.then(data => console.log(data)); // Uncaught (in promise) Error: Uh oh!
p
.then(data => console.log(data))
.catch(err => console.error(err)); // Catch the error!
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.
// Class declaration
class Animal {
}
// Class expression
const Animal = class {}
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks!`);
}
}
const puppy = new Dog('Spot');
puppy.speak(); // Spot barks!
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
this.breed = breed;
}
speak() {
console.log(`${this.name} barks!`);
}
}
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!
const getUser = username => {
return fetch(`https://api.github.com/users/${username}`)
.then( response => {
if ( ! response.ok ) {
throw new Error( `HTTP error! Response status: ${response.status}` );
}
return response.json();
} )
.catch( e => console.error( e ) );
}
const user = getUser( 'kevinlangleyjr' );
console.log( user );
And now, let's turn change this to use async/await.
const getUser = async username => {
try {
const response = await fetch(`https://api.github.com/users/${username}`)
if ( ! response.ok ) {
throw new Error( `HTTP error! Response status: ${response.status}` );
}
const user = await response.json();
return user;
} catch ( e ) {
console.error( e );
}
}
const user = getUser( 'kevinlangleyjr' );
console.log( user );
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.
const set = new Set();
set.add( 9 ); // Set(1) { 9 }
set.add( 9 ); // Set(1) { 9 } Ignored because it already exists in the set.
set.add( 7 ); // Set(2) { 9, 7 }
set.add( 7 ); // Set(2) { 9, 7 } Ignored because it already exists in the set.
set.add( 'Text' ); // Set(3) { 9, 7, 'Text' }
set.add( true ); // Set(4) { 9, 7, 'Text', true }
const user = {
name: 'Kevin',
location: 'Florida'
};
set.add( user ); // Set(5) { 9, 7, 'Text', true, { value: { location: 'Florida', name: 'Kevin' } } }
set.has( 9 ); // true
set.has( true ); // true
const newUser = {
name: 'Kevin',
location: 'Florida'
};
set.has( newUser ); // false - This doesn't do object property matching, it is an object reference check.
set.has( user ); // true
// Use .entries() to capture all the values in the set.
const entries = set.entries();
console.log( entries ); // SetIterator {9 => 9, 7 => 7, 'Text' => 'Text', true => true, {…} => {…}}
set.delete( true ); // true - This change is still reflected in entries because it keeps the reference
set.has( true ); // false
console.log( entries ); // SetIterator {9 => 9, 7 => 7, 'Text' => 'Text', {…} => {…}}
set.size // 5
// Iterating over a set is pretty simple but you have a few options.
for ( let item of set.values() ) {
console.log( item );
}
// Is the same as,
for ( let item of set.keys() ) {
console.log( item );
}
// And is also the same as,
for ( let [key, value] of set.entries() ) {
console.log( key );
}
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.
let ws = new WeakSet();
let user = { name: 'Kevin', location: 'Florida' };
ws.add( user );
console.log( ws.has( user ) ); // true
user = null; // The reference to `user` within the WeakSet will be garbage collected shortly after this point.
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.
const map = new Map();
map.set( 1, 'Learn' );// Map(1) {1 => 'Learn'}
map.set( 2, 'JavaScript' );// Map(2) {1 => 'Learn', 2 => 'JavaScript'}
map.set( 3, 'Deeply' );// Map(3) {1 => 'Learn', 2 => 'JavaScript', 3 => 'Deeply'}
map.set( 3, 'Very Deeply' );// Map(3) {1 => 'Learn', 2 => 'JavaScript', 3 => 'Very Deeply'} - Overwrite value of Deeply set on line above.
map.set(
'1',
'Different string value for the string key of 1.'
); // Map(4) {1 => 'Learn', 2 => 'JavaScript', 3 => 'Very Deeply', '1' => 'Different string value for the string key of 1.'}
map.set(
true,
'Different string value for the bool of true.'
); // Map(5) {1 => 'Learn', 2 => 'JavaScript', 3 => 'Very Deeply', '1' => 'Different string value for the string key of 1.', true => 'Different string value for the bool of true.'}
map.size // 5
map.delete( true ); // Map(4) {1 => 'Learn', 2 => 'JavaScript', 3 => 'Very Deeply', '1' => 'Different string value for the string key of 1.'}
map.delete( '1' ); // Map(3) {1 => 'Learn', 2 => 'JavaScript', 3 => 'Very Deeply'}
map.clear(); // Map(0) {size: 0}
/* New example with objects for keys. */
const user1 = { name: 'James' };
const user2 = { name: 'Kevin' };
// New Map for votes.
const votes = new Map();
votes.set( user1, 10 ); // Map(1) {{ name: 'James' } => 10}
votes.has( user1 ); // true
votes.has( user2 ); // false - Not set yet.
votes.set( user2, 20 ); // Map(2) {{ name: 'James' } => 10, { name: 'Kevin' } => 20}
votes.has( user1 ); // true
votes.has( user2 ); // true
votes.get( user1 ); // 10
votes.get( user2 ); // 20
votes.size // 2
References
Posted on December 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.