Modern JS with ES6+

kevinlangleyjr

Kevin Langley Jr

Posted on December 17, 2021

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.

// mathlib.js

export function square(x) {
  return x * x;
}

export function add(x, y) {
  return x + y;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Default Export

With default exports, you just prepend what you want to export with export default.

// foo.js
export default () => {
  console.log('Foo!');
}
Enter fullscreen mode Exit fullscreen mode
// main.js
import foo from './foo';

foo(); // Foo!
Enter fullscreen mode Exit fullscreen mode

Default and Named Exports

The two ways can even be mixed!

// foobar.js

export default () => {
  console.log('Foo!');
}

export const bar() => {
  console.log( 'Bar!' )
}
Enter fullscreen mode Exit fullscreen mode
// main.js

import foo, { bar } from './foobar';

foo(); // Foo!

bar(); // Bar!
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

let and const are block scoped.

if ( true ) {
  let foo = 'bar';
  const bar = 'foo';
}

console.log( foo ); // ReferenceError.
console.log( bar ); // ReferenceError.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

const variables can only be assigned once. But it is NOT immutable.

const foo = { bar: 1 };

foo = 'bar'; // 'foo' is read only.
Enter fullscreen mode Exit fullscreen mode

But, you can change the properties!

const foo = { bar: 1 };

foo.bar = 2;
console.log(foo); // { bar: 2 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Another option you have is to seal an object to prevent changing the object structure.

Seal
Photo Credit: Eva Rinaldi

Wait, no. Not that Seal!

Seal
Photo Credit: Amy Asher

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 }
Enter fullscreen mode Exit fullscreen mode

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!');
}
Enter fullscreen mode Exit fullscreen mode
console.log( foobar ); // undefined (note: not ReferenceError!)
var foobar = 'Woot!'
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Temporal Dead Zone

TARDIS
Photo Credit: Giles Turnbull

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!
}
Enter fullscreen mode Exit fullscreen mode

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 use let if rebinding is needed.var shouldn't be used in ES2015.

Mathias Bynens - V8 Engineer @ Google

Use var for top level variables Use let for localized variables in smaller scopes. Refactor let to const 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

Mind Blown Gif

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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}!` );
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
function Person(){
  var that = this;
  this.age = 0;

  setInterval(function() {
    that.age++; // Without arrow functions. Works, but is not ideal.
  }, 1000);
}
Enter fullscreen mode Exit fullscreen mode
function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // `this` properly refers to the person object. 🎉🎉🎉
  }, 1000);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

A little better

function calculateTotal( subtotal, tax, shipping ) {
  tax = tax || 0.07;
  shipping = shipping || 10;

  return subtotal + shipping + (subtotal * tax);
}

const total = calculateTotal(100);
Enter fullscreen mode Exit fullscreen mode

Now with ES6+! 🎉🎉🎉

function calculateTotal( subtotal, tax = 0.07, shipping = 10 ) {
  return subtotal + shipping + (subtotal * tax);
}

const total = calculateTotal(100);
Enter fullscreen mode Exit fullscreen mode

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); // 🎉🎉🎉🎉
Enter fullscreen mode Exit fullscreen mode

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'
  }
};
Enter fullscreen mode Exit fullscreen mode

Using the above object, let's create some variables from the object's properties.

const first = person.first;
const last = person.last;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

It even works with nested properties!

const { first, last, location: { city } } = person;

console.log( city ); // Beverly Hills
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

...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);
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode
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 }
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

...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);
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode
const name = 'Kevin';

// With ES6 template literals.
console.log(`Hello, ${name}!`); // Hello, Kevin!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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 )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

.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 )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

.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 )
Enter fullscreen mode Exit fullscreen mode

Examples

const str = 'Learn JavaScript Deeply';

console.log(str.includes('JavaScript'));// true
console.log(str.includes('Javascript'));// false
console.log(str.includes('PHP'));// false
Enter fullscreen mode Exit fullscreen mode

.repeat()

This method returns a new string which contains a concatenation of the specified number of copies of the original string.

Syntax

repeat( count )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Enhanced Object Literals

const first = 'Kevin';
const last = 'Langley';
const age = 29;
Enter fullscreen mode Exit fullscreen mode

Let's assign our variables to properties of an object!

const person = {
  first: first,
  last: last,
  age: age
};
Enter fullscreen mode Exit fullscreen mode

Now let's do it again but with object literals this time.

const person = {
  first,
  last,
  age
};
Enter fullscreen mode Exit fullscreen mode

You can even mix object literals with normal key value pairs.

const person = {
  firstName: first,
  lastName: last,
  age
};
Enter fullscreen mode Exit fullscreen mode

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');

}

};
Enter fullscreen mode Exit fullscreen mode

Can now be simplified with the new syntax for method definitions!

const obj = {
  foo() {
    console.log('foo');
  },
  bar() {
    console.log('bar');
  }
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!"}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

Array.from()

The Array.from static method let's you create Arrays 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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Examples

const values = Array.of(123, 456, 789);

console.log(values); // [123,456,789]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Examples

const values = Array.of(123, 456, 789);
console.log(values); // [123,456,789]
Enter fullscreen mode Exit fullscreen mode

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 );
} );
Enter fullscreen mode Exit fullscreen mode

Promise Harry Potter Gif

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
Enter fullscreen mode Exit fullscreen mode

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;
  } );
Enter fullscreen mode Exit fullscreen mode

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", ...}
// ...
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode
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!
Enter fullscreen mode Exit fullscreen mode
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!`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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 );
Enter fullscreen mode Exit fullscreen mode

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 );
Enter fullscreen mode Exit fullscreen mode

The difference being, we're now wrapping the entire function body where we do our awaits 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 );
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.

Force JavaScript garbage collection in Chrome

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
Enter fullscreen mode Exit fullscreen mode

References

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
💖 💪 🙅 🚩
kevinlangleyjr
Kevin Langley Jr

Posted on December 17, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related