Grow with Google: ES6

Picking up where I left off in Part 2, I’m continuing my journey in the Grow with Google Challenge Scholarship for 2018. Last time I learned all about IndexedDB, but this part of the course taught me ES6, the 6th edition of JavaScript, which is widely used today (sometimes with other names, like ES2015 and Harmony). These lessons gave me a whole new perspective on the language, and they’ve answered many long-time questions I’ve had about the “magic” of JavaScript.

A great part about these lessons is that they were abstract and generic, not focused on a specific project like the lessons before. As a result, it’s easy for me to demonstrate the concepts for you this time!

Syntax

Instead of using var to declare variables, it’s recommended to use let and const now. Use let when you plan to assign new values to the variable, and use const when you only want to declare it once. Using let and const prevents namespace pollution in the global scope, because variables declared with these commands are limited to the block that contains them. There’s no reason to use var anymore.

It’s now recommended to perform string interpolation with template literals. Before, if you wanted to insert variables into a string, it required clumsy syntax, like:

console.log('Today is ' + calendar.returnDay() + '.');

Now you can use template literals, indicated with backticks (`` ) instead of quotes ('' or ""). They allow you to insert values into a string via ${expression}. Now you can represent that same string like so:

console.log(`Today is ${calendar.returnDay()}.`);

You can now extract values from arrays and objects through a technique called destructuring, whereby you specify the elements to be extracted on the left side of the assignment. For example:

const prices = [12, 16, 70];
const [cheap, medium, expensive] = prices;

console.log(cheap, medium, expensive);

Produces the following output: 12 16 70

Now thanks to object literal shorthand, you can declare objects with simpler code. If there already exist variables within scope that share the same name as the object property, you don’t have to declare both sides of the assignment. You can also drop the function keyword from function declarations inside of an object, as long as you have (). For example:

const color = 'red';
const material = 'wax';
const crayon = {
  color: color,
  material: material,
  printDescription: function() {
    console.log(`I'm ${color} and made of ${material}.`);
  }
};

crayon.printDescription();

Can be reduced to:

const color = 'red';
const material = 'wax';
const crayon = {
  color,
  material,
  printDescription() {
    console.log(`I'm ${color} and made of ${material}.`);
  }
};

crayon.printDescription();

And they both output: I'm red and made of wax.

Next, we now have the superior for…of loop to save us from the drawbacks of for and for…in loops. The for loop requires a counter and an exit condition, wheras the for…in loop removes those requirements by using the index. But using the for…in loop makes a mess because if you add a new method to the prototype, it will appear in the loop. But the for…of loop solves these problems with very similar execution as for…in, but without the drawbacks. Let’s start with a for loop:

const nums = [1, 2, 3, 4, 5];
for (let i = 0; i < nums.length; i++){
  console.log(nums[i]);
}

Here’s the same loop, but as a for…in loop:

const nums = [1, 2, 3, 4, 5];
for (const index in nums){
  console.log(nums[index]);
}

And finally, here’s the same loop as the superior for…of loop:

const nums = [1, 2, 3, 4, 5];
for (const num of nums) {
 console.log(num);
}

They all produce the same output:

1
2
3
4
5

The for…of loop only iterates over values in the object, so you’re free to add methods. You can also pass over certain values during iteration with the continue command. For example:

const nums = [1, 2, 3, 4, 5];
for (const num of nums) {
  if (num % 2 === 0) {
    continue;
  }
  console.log(num);
}

Will produce the following output:

1
3
5

We now have spread and rest. They’re written with ... and to start, spread allows you to spread iterable objects into multiple elements. For example:

const children = ['Tabatha', 'Riley', 'Jordan'];
console.log(...children);

Will output:

Tabatha Riley Jordan

You can use the spread operator to concatenate arrays easily. For example, if you have two arrays and want to combine their entries:

const children = ['Tabatha', 'Riley', 'Jordan'];
const cousins = ['Brandon', 'Ysabel', 'Kyle'];
const family = [...children, ...cousins];

console.log(family);

Will output:

["Tabatha", "Riley", "Jordan", "Brandon", "Ysabel", "Kyle"]

Just remember that this is an operator, not a reference, so if you later make a change to either the children or cousins array, it will not update the assigned variable (family in this case) because the operation only happened once at assignment.

To balance spread, we have the rest parameter, which allows you to represent any amount of elements as an array. This is useful for destructuring and building variadic functions. For example, you can use it to accept a limitless amount of arguments for a function:

function multiply(...nums) {
  let total = 1;
  for(const num of nums) {
    total = total * num;
  }
  return total;
}

console.log(multiply(1,2,3,4,5));

Will output:

120
Functions

Now in ES6 we have a new type of function called an arrow function. An arrow function is shorthand for calling a new function, and is mostly the same. A major difference is that these two types of functions use different this parameters. To illustrate, first see a function declared normally:

const firstThree = ['Joseph', 'Rachel', 'Melody'].map(function(name) {
   return name.slice(0,3);
});

console.log(firstThree);

And the following is the same function but using an arrow function:

const firstThree = ['Joseph', 'Rachel', 'Melody'].map(
  name => name.slice(0,3)
);

console.log(firstThree);

They both output:

["Jos", "Rac", "Mel"]

The this parameter for each of them is different. If the object was called with new then this would be that object. Otherwise, this depends on the context of the method and if it was used with call() or apply(). So in a normal function, this is determined by how the function is called. But for arrow functions, this is determined by the context of where the function is called.

We can also use default function parameters to provide fallback values for our function, allowing it to be called in a wider variety of situations. For example:

function eat(food = 'salad', size = 'medium') {
  return `Please enjoy your ${size} ${food}.`;
}

console.log(eat());
console.log(eat('steak', 'large'));
console.log(eat('steak'));
console.log(eat(undefined, 'large'));

Will output:

Please enjoy your medium salad.
Please enjoy your large steak.
Please enjoy your medium steak.
Please enjoy your large salad.

But if you want to set the size of the meal but keep the default food, you have to pass undefined through the function’s first argument when using an array of arguments like this. Instead of taking this tedious approach, we can combine default function parameters with destructuring to make powerfully flexible functions. If we pass an object with properties to the function, we can destructure the object’s properties inside the function, and provide defaults for any that might be missing. Since they’re matched by property name, they can be inserted in any order or in any combination. You can even provide a default object as an argument for the function to allow it to be called with just () with default values. For example:

function buildShip({sails = 4, size = 'medium', purpose = 'trade', coffers = 500, captain = 'Saltybeard'} = {}) {
  return `You build a ${size} ship with ${sails} sails. You appoint ${captain} as the captain and fill the coffers with ${coffers} pewter coins. You tell the captain to pursue ${purpose}.`;
}

console.log(buildShip());
console.log(buildShip({sails : 5, purpose : 'war'}));
console.log(buildShip({captain : 'Peggy', coffers : 750}));

Will output:

You build a medium ship with 4 sails. You appoint Saltybeard as the captain and fill the coffers with 500 pewter coins. You tell the captain to pursue trade.
You build a medium ship with 5 sails. You appoint Saltybeard as the captain and fill the coffers with 500 pewter coins. You tell the captain to pursue war.
You build a medium ship with 4 sails. You appoint Peggy as the captain and fill the coffers with 750 pewter coins. You tell the captain to pursue trade.

In ES6 we also get classes for the first time in JavaScript, which can be deceptive! These new classes are still just regular functions in disguise. You can designate constructor functions in a class declaration, and nest functions and variables in a style similar to other languages at this level of abstraction. However, it’s important to note that this does not change the pre-existing functionality of JavaScript functions, and we still use functions to create objects. You can extend classes with extends and you can use super as either an object or a function to interact with the a child function’s parent. The syntax is also slightly different from declaring normal objects or functions. To demonstrate all of this:

class Vehicle {
  constructor(wheels = 4, gasoline = 15, color = 'silver') {
    this.wheels = wheels;
    this.gasoline = gasoline;
    this.color = color;
  }

  drive() {
    if (this.gasoline > 0) {
      console.log(`You use some gas to drive your ${this.color} vehicle further.`);
      this.gasoline--;
      console.log(`You now have ${this.gasoline} gallons of gas left.`);
    }
    else {
      console.log('You are out of gas!');
    }
  }
}

class Motorcycle extends Vehicle {
  constructor(wheels = 2, gasoline = 8, color) {
    super(wheels, gasoline, color);
  }

  drive() {
    if (this.gasoline > 0) {
      // a motorcycle only consumes half a gallon per drive,
      // so we need to offset the parent function if there's
      // enough gas to drive
      this.gasoline += 0.5;
    }
    super.drive();
  }

  doWheelie() {
    console.log('You do a cool wheelie!');  }
}

const myCar = new Vehicle();
const myTruck = new Vehicle(6, 0, 'blue');
const myMotorcycle = new Motorcycle(undefined, undefined, 'yellow');

myCar.drive();
myTruck.drive();
myMotorcycle.doWheelie();
myMotorcycle.drive();

Will output:

You use some gas to drive your silver vehicle further.
You now have 14 gallons of gas left.
You are out of gas!
You do a cool wheelie!
You use some gas to drive your yellow vehicle further.
You now have 7.5 gallons of gas left.
Built-ins

We now have symbols which are unique, immutable data types. To create a symbol you use the Symbol() syntax and you can pass it an optional description, like Symbol('frog'). Descriptions do not assign value to the symbol, so two symbols with identical descriptions do not equal each other. Symbols become useful for situations like adding duplicate properties to objects. For example:

const lake = {
  [Symbol('frog')]: { sound: 'ribbit', diet: 'bugs' },
  [Symbol('snake')]: { sound: 'hiss', diet: 'children' },
  [Symbol('frog')]: { sound: 'ribbit', diet: 'bugs' }
}

This allows us to add two separate properties with the descriptor ‘frog’ by giving them unique references via symbols.

Now in ES6 we have the flexibility to define our iteration behavior. By adhering to the iterable protocol you can define and customize the iteration behavior of objects. Any object that is iterable can use the for…of loop. To implement the iterable interface, you can access the iterator method via the constant [Symbol.iterator], which is a function that returns an iterator object. The iterator protocol can be defined by implementing the .next()function on this object.

Sets are a new object that can enforce a collection of distinct entries, like a mathematical set. To contrast, const array = [0, 1, 3, 4, 4]; can have any amount or combination of data, and adding more like array.push(4);won’t raise any complaints. If you need to enforce uniqueness within the collection, you can use a Set. It’s important to note that Sets are not indexed based and you cannot access the entries individually. You can iterator over entries, add new ones, delete them, and clear the collection.

By using a Set, you gain access to a new group of functions for working with the entries. You can use .size() instead of .length() to return the length of the Set, and it’s very easy to use .has() to search the Set for an entry. It will return true if it found the value. You can use the .values() or .keys() functions interchangeably to return an iterator object (like above) which you can iterate over via .next() or a simple for…of loop. For example:

const planets = new Set(['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune']);

for (const planet of planets) {
  console.log(planet);
}

Will output:

Mercury
Venus
Earth
Mars
Jupiter
Saturn
Uranus
Neptune

Sets have a sibling called WeakSets which are more or less the same, but have a few more rules. They can only contain objects, they cannot be iterated over, and they cannot be cleared. Objects are removed from a WeakSet when they are garbage collected. When the object represented in the WeakSet is set to null then the object will be garbage collected and removed from the WeakSet.

Sets and WeakSets have close cousins called Map and WeakMap. A Set is to an array as a Map is to an object. Maps contain key-value pairs, much like objects contain properties with values. The keys and values can be represented as objects or values indiscriminately. You can’t assign values to a Map at declaration, they have to be added in with the .set(key, value)function. You can iterate over a Map like a Set, as well. For example:

const wealth = new Map();

wealth.set('Acme', 31.68);
wealth.set('Weyland', 90.34);
wealth.set('Yutani', 17.26);
wealth.set('Ecorp', 23.25);

for (const company of wealth) {
  const [key, value] = company;
  console.log(key, value);
}

Will output:

Acme 31.68
Weyland 90.34
Yutani 17.26
Ecorp 23.25

WeakMaps are just like WeakSets and have the same restrictions and garbage collection features. The key argument in a WeakMap’s .set(key, value) function must be an object.

JavaScript now has Promises with ES6. A Promise lets you execute asynchronous code and call a function upon completion (or failure). We can attach callback functions to the object returned by the Promise via the .then() method. Tools like Proxies let you create “middlemen” that stand between any object and handler. Proxies are more powerful than getter and setter functions because you don’t have to know the properties beforehand. You can rig Proxies with traps to trigger on certain events on certain objects, allowing the Proxy to intercept the function.

Perhaps my favorite addition to the language, we now have generators which allow us to control the flow of execution. Generator functions can be paused mid-execution, and they’re indicated with an *, like so: function* getDoctors(){ ... } (note that the * can be surrounded by spaces, or smushed right between the two words, just as long as it’s there). You can use the .next() function on a generator to continue the execution (which begins paused). To insert a breakpoint, use the yield syntax. A cool thing about generators is you can send data back into the function via the .next()method and send it out by attaching it to yield. For example:

function* getEmployee() {
  const planets = ['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune'];   
  const order = [];
  for (const planet of planets) {
    order.push(yield planet);
   }
  return order;
}
const generatorExample = getEmployee();

let planet = generatorExample.next().value;
planet = generatorExample.next(`The first planet is ${planet}.`).value;
planet = generatorExample.next(`The second planet is ${planet}.`).value;
planet = generatorExample.next(`The third planet is ${planet}.`).value;
planet = generatorExample.next(`The fourth planet is ${planet}.`).value;
planet = generatorExample.next(`The fifth planet is ${planet}.`).value;
planet = generatorExample.next(`The sixth planet is ${planet}.`).value;
planet = generatorExample.next(`The sevent planet is ${planet}.`).value;

const solarSystem = generatorExample.next(`The eighth planet is ${planet}.`).value; solarSystem.join('\n');console.log(solarSystem);

Will output:

["The first planet is Mercury.", "The second planet is Venus.", "The third planet is Earth.", "The fourth planet is Mars.", "The fifth planet is Jupiter.", "The sixth planet is Saturn.", "The seventh planet is Uranus.", "The eighth planet is Neptune."]
Transpiling

To take advantage of these new ES6 features on the web, they have to be supported by the user’s browser. You can check out this browser compatibility table to see what ES6 features are supported on what browser versions. I’ve also included links to each major browser’s platform status in the resources at the end of this article. If a feature you use is not supported by a target browser, you have to transpile your code into ES5 prior to deploying.

According to the W3 Global Web Stats, less than 4% of web surfer still use IE11 (which fails most ES6 checks). However, new bleeding edge JavaScript features come out all the time, and to use them in a deployment environment for most users, you will want to consider transpiling your code with a tool like Babel for most browsers.

Thankfully in 2018, most ES6 features are supported by most modern browsers, so this isn’t always a necessity. You can also target specific functionality via polyfills, which stand in for newer code. Modernizr has collected a list of polyfills, and I recommend checking them out.

I’ve just scratched the surface, so I recommend checking out the full list of ES6 features to learn more about what’s supported. Now that I’ve finished all of the lesson modules, I have effectively finished my challenge course for Google and Udacity. At this point, I have to wait until April 11th for the program to end. Announcements for who made the top 1,000 (and win a free Nanodegree program) will come out on April 17th. I plan to spend my time between now and then reflecting on these lessons, completing more Udacity courses, and applying what I’ve learned towards some new projects.

Click here to read Part 4!

Resources

ECMAScript 6 Features

ECMAScript 6 Browser Compatibility Table

Babel

Chrome Platform Status (for ES6)

Edge Platform Status (for ES6)

Firefox Platform Status (for ES6)

WebKit (i.e. Safari) Feature Status (for ES6)

Remy Sharp: What is a Polyfill?

Modernizr: HTML5 Cross Browser Polyfills

Ecma International: ECMAScript 2015 Language Specification

Udacity/Google: ES6 - JavaScript Improved