JavaScript (ES2015+) Enlightenment

Grokking Modern JavaScript, In The Wild

Written by Cody Lindley


Sponsored by Frontend Masters, advancing your skills with in-depth, modern front-end engineering courses

Today, tools like Babel have made it commonplace to see ES2015, ES2016, ES2017, ES2018, and ES2019 language updates/proposals in babelified source code. These compounding language changes can make it difficult to learn something like React, Apollo GraphQL, or Webpack.
This book aims to alleviate this problem by providing a curated selection of the commonly used language updates, tersely explained, to lessen this indirection. Thus, after studying the material in this book grokking new JavaScript code while learning JavaScript frameworks and tools, should be much more comfortable.

Written For:

The contents of this book are for developers who are working in a codebase using modern React, Vue, or Angular code and find recent JavaScript language updates/proposals to be causing too much indirection. And or, developers who want to drill into memory the latest and most commonly used JavaScript updates.

ES2015+ Enlightenment is not a rudimentary read on the JavaScript language. The content in this book attempts to take a developer with ES3 and ES5 knowledge and make them more knowledgeable about ES2015+ and the implications of modern changes to the language on JavaScript tools and frameworks.


How to Use/Read This Book:

First off, this is a mix between a book, a reference, and cheatsheet. My intention in writing is to shine a light on ES5+ language changes in a tersely and helpfully format. To be clear this is not a long form book on the JavaScript language. Or, a detailed reference. Consider this an elaborate cheatsheet with runnable code purposefully curated for those who know ES3 but need to master ES5+.

Second, this is a web book. A lot of contexts can be gained by just clicking on links in this book. If you ever feel in need of more context use the links in the text.


How to Use/Read The Code Examples:

Try and view the code examples as an extension of the words. First, read and re-read the words. Then read the code, especially the code comments, from top to bottom as if they are part of the surrounding paragraphs. The goal should be to grok the code until no questions remain as to what the code example is doing and expressing.


While Using/Reading the book remember, by design:

  1. The words and code comments are intentionally terse with the goal of code comprehension without long-winded and exhaustive explanations.
  2. The code examples are contrived to reveal the nature of the code. Focus on what the code is doing and making sure you understand it, potentially over my words.
  3. The book is a mix of a mini book, a reference, and cheatsheet. Expect it to feel like one of these or all of these at the same time.

Contribute content, suggestions, and fixes on github:

https://github.com/FrontendMasters/javascript-enlightenment


Chapter 1 : ECMAScript 5 (aka ES5) Recap

In this chapter, I'll recap the significant language updates introduce in ES5 to delineate these updates from the updates made in ES2015 (aka ES6).

1.1 : ES5 Browser and Node Compatibility

For the most part, ES5 is compatible with modern browsers (e.g. IE9+, excluding strict mode) and Node since version 4.x.x.

Unless you have to support an older JavaScript engine/runtime (e.g. IE8) you are safe to assume most modern JavaScript engines/runtimes support ES5.

1.2 : New ES5 String Method

The ES5 .trim() method removes whitespace from both ends of a string and creates a new string.


var myString = '  Some Tabs and Spaces      ';
console.log(myString.length); // logs 28
var myNewString = myString.trim(); // trim it
console.log(myNewString); // logs 'Some Tabs and Spaces'
console.log(myNewString.length); // logs 20

// Note: this method does not mutate a value it creates a new value
console.log(myString, myString.length); // This still is, '  Some Tabs and Spaces      '
    

You should consider "Whitespace" to mean in general; spaces, tabs, and non-breaking spaces used in a string.

Specifically trim() removes:

  • \U0009 character tabulation
  • \U000A line feed
  • \U000B line tab
  • \U000C form feed
  • \U000D carriage return
  • \U0020 space
  • \U3000 ideographic space
  • \UFEFF zero-width non-breaking space

1.3 : New ES5 Array Static Methods

ES5 added the static Array method, Array.isArray().

The Array.isArray() method is used to determine precisely (true or false) if a value is a true Array. In other words, this method checks to see if the provided value is an instance of the Array() constructor.


console.log(Array.isArray([1,2,3])) //logs true

// Note: does not work on Array-like objects
console.log(Array.isArray({length: 3, 0:1, 1:2, 2:3})) //logs false
    

Notes:

  1. The static Array.isArray() method differs from using [] instanceof Array only slightly when dealing with iframes.
  2. This isArray() method also respects values that are constructed from constructors extended from the native Array constructor using the new class extends keyword.

1.4 : New ES5 Array Methods

ES5 added the following Array methods (i.e. higher-order iteration functions):

  • [].some()
  • [].every()
  • [].filter()
  • [].forEach()
  • [].indexOf()
  • [].lastIndexOf()
  • [].map()
  • [].reduce()
  • [].reduceRight()

The [].some() method will start testing values in array, until a test returns true, then the function passed to .some() immediately returns true, otherwise the function returns false (i.e. the first truthy value found will result in the function immediately returning true and potentially this could mean not all tests are run).


// Check if one or more items in the array is bigger than or equal to 2
var someMethod = [1, 2, 3].some(function(value, valueIndex, wholeArray){
    return value >= 2;
});

console.log(someMethod)
// logs true because the array contains a value that is greater than or equal to 2
    

The [].every() method will start testing values in array, until a test returns false, then the function passed to .every() immediately returns false, otherwise the function returns true (i.e. the first falsy value found will result in the function immediately returning false and potentially this could mean not all tests are run).


// Check if every item in the array is bigger than or equal to 2
var everyMethod = [1, 2, 3].every(function(value, valueIndex, wholeArray){
    return value >= 2;
});

console.log(everyMethod) // logs false because the array contains a value that is less than 2
    

The [].filter() method will return a new Array containing all the values that pass (i.e. are true) the filtering test.


var myArray = [1,2,3];
// filter out any value in the array that is not bigger than or equal to 2
var FilteredArray = myArray.filter(function(value, valueIndex, wholeArray){
    return value >= 2;
});

console.log(FilteredArray) // logs [2,3]

// Note: filter() returns a new Array, myArray is still equal to [1,2,3]
    

The [].forEach() method executes a provided function for each value in the array.


// log to the console each value, valueIndex, and wholeArray passed to the function
['dog','cat','mouse'].forEach(function(value, valueIndex, wholeArray){
    console.log('value = '+value+' valueIndex = '+valueIndex+' wholeArray = '+wholeArray);
    /** logs:

    "value=dog valueIndex=0 wholeArray=dog,cat,mouse "
    "value=cat valueIndex=1 wholeArray=dog,cat,mouse "
    "value=mouse valueIndex=2 wholeArray=dog,cat,mouse "

    **/
});
    

The [].indexOf() method searches an array for the first value matching the value passed to indexOf(), and returns the index of this value.


// get index of first 'cat'
console.log(['dog','cat','mouse', 'cat'].indexOf('cat')); // logs 1

// Note: Remember the index starts at 0
    

The [].lastIndexOf() method searches an array for the last value matching the value passed to [].lastIndexOf(), and returns the index of this value.


// get index of last 'cat'
console.log(['dog','cat','mouse', 'cat'].lastIndexOf('cat')); // logs 3

// Note: Remember the index starts at 0
    

The [].map() method executes a provided function for each value in the array, and returns the results in a new array.


var myArray = [5, 15, 25];
// add 10 to every number in the array
var mappedArray = myArray.map(function(value, valueIndex, wholeArray){
    return value + 10;
});

console.log(mappedArray) // logs [15,25,35]

// Note: map() returns a new Array, myArray is still equal to [5, 15, 25]
    

The [].reduce() method runs a function that passes the return value to the next iteration of the function using values in the array from left to right and returning a final value.


// add up numbers in array from left to right i.e. (((5+5) +5 ) + 2)
var reduceMethod = [5, 5, 5, 2].reduce(function(accumulator, value, valueIndex, wholeArray){
    return accumulator + value;
});
console.log(reduceMethod) // logs 17

/** reduce also accepts a second parameter that sets the first accumulator value,
instead of using the first value in the array. **/

// add up numbers in array from left to right, but start at 10 i.e. ((((10+5) +5 ) +5 ) + 2)
var reduceMethod = [5, 5, 5, 2].reduce(function(accumulator, value, valueIndex, wholeArray){
    return accumulator + value; // first iteration of func accumulator is 10 not 5
}, 10);
console.log(reduceMethod) // logs 27
    

The [].reduceRight() method runs a function that passes the return value to the next iteration of the function using values in the array from right to left and returning a final value.


// add up numbers in array from left to right i.e. (((2+5) +5 ) + 5)
var reduceRightMethod = [5, 5, 5, 2].reduceRight(function(accumulator, value, valueIndex, wholeArray){
    return accumulator + value;
});
console.log(reduceRightMethod) // logs 17

/** reduce also accepts a second parameter that sets the first accumulator value,
instead of using the first value in the array. **/

// add up numbers in array from left to right, but start at 10 i.e. ((((10+2) + 5 ) +5 ) + 5)
var reduceRightMethod = [5, 5, 5, 2].reduceRight(function(accumulator, value, valueIndex, wholeArray){
    return accumulator + value; // first iteration of func accumulator is 10 not 5
}, 10);
console.log(reduceRightMethod) // logs 27
    

Notes:

  1. All the new methods ignore holes in arrays (i.e. [1,2,,,,,,,,,3]).
  2. All of these new Array methods, except for reduce and reduceRight accept a second parameter. This second parameter allows you to set the this value for the function being passed in the first parameter.

1.5 : New ES5 Getters and Setters (aka Accessors Descriptors or Computed Properties)

ES5 adds to Objects computed properties via the keywords get and set. This means that Objects can have properties, that are methods, but don't act like methods (i.e you don't invoke them using ()). In short by labeling a function in an object with get or set one can invoke the backing property function on a property, by merely accessing the property, without using innovating brackets.

The example below demonstrates the nature of getter and setter properties:


var obj = {
    get RunsWhenAccessed(){
        console.log('you accessed the property RunsWhenAccessed');
    },
    set RunsWhenSet(newValueBeingSet){
        console.log('you set the property RunsWhenSet to : ' + newValueBeingSet);
    }
}

// access the RunsWhenAccessed property and the backing property function fires
obj.RunsWhenAccessed; // logs 'you accessed the property RunsWhenAccessed'

// access and set the RunsWhenSet property and the backing property function fires
obj.RunsWhenSet = 'foo'; // logs 'you set the property RunsWhenSet to : foo'
// note I am setting a value that becomes an argument and not calling a function using brackets

Don't over think getters and setters, they are simply a property who's value is determined by running a backing property function and the function is invoked by accessing or setting the property.


var person = {
    firstName : '',
    lastName : '',
    get name() {
        return this.firstName + ' ' + this.lastName;
    },
    set name(str) {
        var n = str.split(/\s+/);
        this.firstName = n.shift();
        this.lastName = n.join(' ');
    }
}

// set name, but store first and last separately
person.name = 'Cody Lindley';

// get name, returns firstName and LastName combined
console.log(person.name); // logs 'Cody Lindley'

Notes:

  1. The same property can have a getter and setter.
  2. The get and set syntax is a shortcut for using Object.defineProperty() and Object.defineProperties() to add the get and set property descriptors.
  3. A setter property can only take in a single value and thus a single argument is passed to the backing property function.

1.6 : New ES5 Object Static Methods

  • Object.create()
  • Object.getPrototypeOf()
  • Object.defineProperty()
  • Object.defineProperties()
  • Object.getOwnPropertyDescriptor()
  • Object.getOwnPropertyNames()
  • Object.preventExtensions()
  • Object.isExtensible()
  • Object.sealed()
  • Object.isSealed()
  • Object.freeze()
  • Object.isFrozen()

ES5 added the Object.create() method so objects could be created and their prototypes easily setup. Object.getPrototypeOf() was added to easily get an objects prototype.


// setup an object to be used as the prototype to a newly created myObject object below
var aPrototype = {
    foo: 'bar',
    getFoo: function(){
        console.log(this.foo);
    }
}

// create a new myObject, setting the prototype of this new object to aPrototype
var myObject = Object.create(aPrototype);

// logs 'bar' because myObject uses aPrototype as its prototype, it inherits getFoo()
myObject.getFoo(); // logs 'bar'

// get a reference to the prototype of myObject, using getPrototypeOf()
console.log(Object.getPrototypeOf(myObject) === aPrototype); //logs true
    

ES5 added Object.defineProperty(), Object.defineProperties(), and Object.getOwnPropertyDescriptor() so object properties can be precisely defined (using descriptors) and retrieved. Descriptors provide an attribute that describe a property in an object. The attributes (i.e descriptors) that each property can have are: configurable, enumerable, value, writable, get, and set.


// create an object with a property and value
const myObject = {
    prop1: 'value1'
}

// get the default descriptors for the prop1 property in myObject.
console.log(Object.getOwnPropertyDescriptor(myObject,'prop1'));

/** the above console logs:

[object Object] {
    configurable: true,
    enumerable: true,
    value: "value1",
    writable: true
}

Note that get and set are undefined by default **/

// add a property, 'value2' with descriptors to myObject using Object.defineProperty()
Object.defineProperty(myObject, 'prop2', {
    value: 'value2',
    writable: true,
    enumerable: true,
    configurable: true,
});

// get the descriptors for the prop2 property.
console.log(Object.getOwnPropertyDescriptor(myObject,'prop2'));

// add multiple properties ('prop3' & 'prop4') with
// descriptors to myObject using Object.defineProperties()
Object.defineProperties(myObject, {
    prop3: {
    enumerable: true,
    configurable: true,
    value: 'value3'
    },
    prop4: {
    enumerable: true,
    configurable: true,
    // Note that value and write properties are not added when using the properties set and get
    set: (newValue) => {
      console.log('you set the property prop4 to : ' + newValue);
      },
    get: () => {
      console.log('you accessed the property prop4');
      return 'prop4'; // the get returns the value for prop4 unlike using, value: 'value4'
      }
    }
});

// get the descriptors for the prop3 and prop4 properties

console.log(Object.getOwnPropertyDescriptor(myObject,'prop3'));

console.log(Object.getOwnPropertyDescriptor(myObject,'prop4'));


// Note that prop4's value is based on get and set, not value
console.log(myObject.prop4);

    

Notes:

  1. Using = to assign an object a property and value is a similar routine but not exactly identical to using Object.defineProperty() and Object.defineProperties(). These two methods allow the assignment of a value as well as the defining/retrieval of a properties descriptors and will ignore the prototype chain (i.e. will not look for inherited properties).
  2. ES2017 added the Object.getOwnPropertyDescriptors() static method. This method returns an object containing all the own property descriptors for a given object.
  3. Below are the descriptions/definitions of the attributes of a property that make up a property descriptor:

    value :
    contains the property's value.
    writable :
    contains a boolean indicating whether the value of a property can be changed or written too.
    get :
    reference to the function that is called when a property is read.
    set :
    reference to the function that is called when a property is set to a value.
    configurable :
    contains a boolean indicating whether a property can have its attributes changed and deleted.
    enumerable :
    contains a boolean indicating if a property will show up on certain operations.

ES5 added Object.keys() which returns an Array of non-inherited-enumerable properties of a given object. ES5 added Object.getOwnPropertyNames() which returns an Array of non-inherited properties of a given object regardless of enumerability.


// Create an object
var myObject = Object.create(null); // no prototype used

// Add prop to object created
myObject.myObjectProp1 = 1;

// Define a property using defineProperty()
Object.defineProperty(myObject, 'myObjectProp2', {
    enumerable: false,
    value: 2
});

// Use keys() to get Array of all non-inherited, enumerable properties
console.log(Object.keys(myObject)); // logs ["myObjectProp1"]

// Use getOwnPropertyNames to get Array of all non-inherited properties including non-enumerable
console.log(Object.getOwnPropertyNames(myObject)); // logs ["myObjectProp1", "myObjectProp2"]
    

Notes:

  1. ES2017 added Object.values() which returns an array of a given object's own enumerable property values and Object.entries() which returns an array of a given object's own enumerable properties and values (e.g. [[property:value],[property:value]]).

ES5 provided three Object methods for protecting objects. They are:

  1. Object.preventExtensions(): Stops properties from being added but not deleted.
  2. Object.seal(): Stops properties from being added or configured (i.e. the configurable descriptor attribute for each property is changed to false).
  3. Object.freeze(): Stops properties from being added, configured, or writable (i.e. the configurable and writable descriptor attribute for each property is changed to false)

To compliment these three methods ES5 also added three Object methods for determining the type of protection an object is using. They are:

  1. Object.isExtensible(): Boolean check if an object is extensible.
  2. Object.isSealed(): Boolean checking if an object is sealed.
  3. Object.isFrozen(): Boolean checking if an object is frozen.

1.7 : New ES5 bind() Function Method

Before ES5 functions could only be invoked and given a this value at invocation time using apply() or call(). In other words, these two methods make it possible to call a function and at call time change the value of this for the body of the function. But what if you don't want to invoke the function? And instead, you want to change the value of this for the function when it is called in the future?

ES5 added bind() and this new function method does not invoke a function but instead takes an existing function and from it creates a new function, yet to be called, with a specified value for this inside of the new function.


window.name = 'John'; // Defined in the global scope

var myObject = {name:'Bill'};

var greeting = function(){
    // if the greeting function has a defined this that is not window i.e. global scope
    console.log(this !== undefined && this !== window ?  this.name : window.name);
};

// invoke greeting, where the this context for the greeting function is the global scope
greeting(); //logs John because the value name is in the global scope

// .bind() greetings function this value to myObject
var bindGreetingToObject = greeting.bind(myObject);
// this keyword now points to myObject, and not the window object.

// invoke bindGreetingToObject with the this context being bound to myObject
bindGreetingToObject(); //logs Bill because this value for bindGreetingToObject is bound to myObject
        

Notes:

  1. Don't forget that the main difference between apply() and call() is that apply() takes an Array of arguments passed to the called function while call() takes a list of individual arguments (e.g. arg2, arg3, ... ). Bind() also takes a list of individual arguments (e.g. arg2, arg3, ... ) passed to the new function being called.

1.8 : New ES5 use strict Mode

Adding, 'use strict' to the top of a JavaScript file or as the first line of a function body will change the language to a stricter version of JavaScript. Today, using strict mode isn't typically a decision to be made because ECMAScript modules are implicitly in strict mode. In other words, spinning up a version of say, create-react-app which uses ECMAScript modules will have 'use strict' in play by default due to the fact that ECMAScript modules (i.e. import React from 'react';) uses strict mode implicitly. It is important you are aware of this fact. In other words, JavaScript modules give you strict mode by default.

1.9 : New ES5 JSON methods

JSON.parse() takes a JSON string and returns the JavaScript value(s) described by the string. In other words, JSON.parse() will convert a string of JavaScript in JSON format into real JavaScript values (i.e. Objects, Arrays, Strings, Numbers, Booleans etc...).


var JSONValues = JSON.parse('{"name":"Bill","age":22}') // convert JSON string to JS values
console.log(typeof JSONValues); // logs "object"
console.log(JSONValues.name, JSONValues.age); // logs Bill, 22
    

JSON.stringify() takes JavaScript values and returns a string representing the values.


var JSONString = JSON.stringify({ name: 'Bill', age: 2 }); // Convert JS Object to JSON String
console.log(typeof JSONString) // logs "string"
console.log(JSONString) // logs "{ "name": "Bill", "age":2} "
    

Notes:

  1. Both stringify() and parse() have an optional second function parameter that can be used to augment the result before it is returned.

1.10 : New ES5 Syntax Changes

Trailing commas in Object literals are now ok:


var myObject = {
    name: 'Bill',
    age: 12, // no syntax error
}
    

Notes:

  1. Be aware, trailing commas are not allowed in JSON.
  2. ES2017 will allow trailing commas when defining function parameters or calling a function with arguments. However, calling a function with a comma alone or defining a function parameter as a comma alone will throw a SyntaxError.

Reserved words can now be used as unquoted Object property keys:


// no syntax error when using reserved keywords as property/keys on an object
var myObject = {
    new: 'new',
    class: 'class',
    if: 'if',
    function: 'function'
}
    

1.11 : From ES5 to ES2015. What?

ES2015 was first called ES6 because at the time an update to the 5th edition of ECMAScript would logically be title ECMAScript edition 6 or "ES6". However, a naming tweak for language updates/changes occurred in 2015. It was decided by TC39, the standardization group for JavaScript/ECMA-262, to release stage four proposals once a year (i.e. stage four are approved changes to the language). Given this change, new updates to the language moving forward would be given the titles ES2015 (i.e. ES6), ES2016, ES2017, ES2018 etc... . Basically, language changes/updates are semantically titled under the year in which the update/change becomes standardized.

Notes:

  1. If it is not obvious, it should be noted that just because a JavaScript language update/change has been standardized does not mean those who make use of the ECMAScript standard will implement the updates/change (i.e., adoption of new standards is a slow and often complicated affair e.g., browser compatibility).

Chapter 2 : Running ES2015+ (Compatibility, Compiling, and Polyfills)

Writing and running ES2015+ (i.e. ES2015, ES2016, ES2017, ES2018) code and staged proposals is not as simple as writing some code and then having a web browser or Node run it. To run ES2015+ and staged proposals a pre-compiler and polyfills are needed. This chapter digs into some of these details.

2.1 : ES2015 Native Runtime Compatibility

Native support for ES2015+ (i.e. ES2015, ES2016, ES2017, ES2018 etc...) varies greatly depending upon which JavaScript engine and runtime one is needing to support (e.g. V8 & Node, V8 & Chrome, JavaScriptCore & Safari, SpiderMonkey & Firefox etc..).

In short, both modern day Node and modern web browser engines mostly have full support for ES2015 (starting with Node 6.14.4+, Edge 15, Chrome 47, Safari 10, and Firefox 54). However, things get complicated in terms of compatibility for ES2016+ and staged proposals. This is why a lot of developers side-step chasing compatibility and turn to polyfills and compilers. Polyfills plug JavaScript runtimes environments, at runtime, with newer unsupported API's while a compiler will transform newer unsupported syntax to previous versions of JavaScript (i.e. ES2015+ > ES5). This combination of polyfills and compiling allows developers to write ES2015+ JavaScript today while still supporting current and previous runtimes (i.e. Write new 2015+ JavaScript, transforming it using something like Babel, then it can run it in IE9 with polyfills).

Notes:

  1. The staging of JavaScript proposals/updates is the process that allows JavaScript to change over time. A proposed change to the language starts at stage 0 and is finished when it reaches stage 4 (stages: 0 = strawman, 1 = proposal, 2 = draft, 3 = Candidate, 4 = finished). When a proposal is finished it simply means it is ready to be added to the formal specification. Today, staged proposals regardless of if they have been officially added to the JavaScript specification can be adopted prematurely by developers using polyfills and compilers (e.g. Both TypeScript and Babel can be configured to interpret staged proposals).

2.2 : Running ES2015+ Using Online Tools

The simplest way to run ES2015+ code online (including staged proposals) is to use the online Babel REPL.

If you'd like to run ES2015+ code in the context of the web platform try the codesandbox.io tool. The vanilla sandbox uses Parcel which uses Babel and babel-preset-env by default. Most of the code examples in this book can be run in codesandbox.io by clicking on the "run/edit in codesandbox.io" link above the code.

2.3 : Running ES2015+ locally

A common way to run ES2015+ code on your local computer, if you are already familiar with Node.js and REPL's, is to use babel-node. Babel-node is a node CLI tool that will compile ES2015+ syntax to ES5 syntax before running it. It can be used in place of the Node.js CLI to run JavaScript code using the Node runtime. Keep in mind that most of ES2015 has been supported in Node since 6.14.4+. But if you want ES2015+ including staged proposals you'll need to use a tool like babel-node.

Personally, I prefer using Quokka.js in my code editor with Babel enabled. I setup Quokka to use babel-preset-env and stage 1-3.

Notes:

  1. Writing source code that uses ES2015+ in a coding environment isn't the topic of this book. However, keep in mind, that most developers today working on the front-end will set up a compiler like Babel or TypeScript as part of a development environment process so ES2015+ code will be compiled as it is developed. Typically, this involves using a module bundler that makes use of Babel or Typescript during development and production bundling.
  2. Babel Polyfill is automatically loaded when using babel-node.

2.4 : Compiling ES2015+ Development Syntax To Static ES5 Production Syntax

Because developers today don't want to wait for native support for newer ES2016, ES2017, ES2018 syntax and coming syntax proposals they will adopt a compiling step for JavaScript source code. A compiling step takes ES2015+ syntax, and potentially staged syntax proposals, and transforms newer/proposed syntax to ES5 syntax (polyfill require for complete compatibility). For example, it can take arrow function syntax and convert it to ES5 syntax.


// Compilers like Babel/TypeScript will take this:
const myFunction = () => {};

// And turn it into this:
var myFunction = function myFunction() {};

The most common compilers in use today are Babel and TypeScript. These compilers are routinely used as part of a build/bundling module process where ES2015+ source code and ES modules syntax are taken in and transformed to static ES5 production code (e.g. Webpack and Parcel exist to bundle assets, but they can also compile ES2015+ code found in JavaScript modules to ES5 when bundling).

Notes:

  1. The Babel compiling tool does double duty by compiling not just ES2015+ to ES5 but also things like JSX to ES5 and Flow to ES5 using plugins.
  2. Compiling does not come without caveats.
  3. Babel and TypeScript differ in the fact that Babel is not trying to be a superset of JavaScript. In other words, Babel does not exist so non-standard language features can be bolted on to the language. However, TypeScript views non-standard language updates as a core part of its purpose for existing (e.g. built in static type checking).

2.5 : Compiling ES2015+ Syntax Dynamically at Runtime

Compilers like Babel are typically used by tools like Webpack or Parcel during module bundling to create static files that are then used in production. However, Babel can also be used dynamically at runtime via babel-standalone. Below is a example of using babel-standalone in a web browser via a .html document.


<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
</head>

<body>

    <div id="output"></div>
    <!-- Load ES2015+ Polyfill, core-js used by Babel polyfill -->
    <script src="https://unpkg.com/core-js-bundle@3.0.0-beta.3/minified.js"></script>
    <!-- Load Babel -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <!-- Your custom script here -->
    <script type="text/babel">

    // Arrow function from ES2015
    const getMessage = () => "Hello World";
    document.getElementById('output').innerHTML = getMessage();

    // .flat() from staged 3 proposal works because of polyfill
    console.log([1, [2, 3], [4, 5]].flat());
    </script>

</body>

</html>

Using Babel dynamically at runtime is generally not recommended for production but it does have a few use cases:

  1. Sites like JSFiddle, JS Bin, the REPL on the Babel site, etc. These sites compile user-provided JavaScript in real-time.
  2. Apps that embed a JavaScript engine such as V8 directly, and want to use Babel for compilation
  3. Apps that want to use JavaScript as a scripting language for extending the app itself, including all the goodies that ES2015 provides.
  4. Integration of Babel into a non-Node.js environment (ReactJS.NET, ruby-babel-transpiler, php-babel-transpiler, etc).

2.6 : Polyfill'ing JavaScript API's at Runtime

Polyfills are JavaScript code used to plug or fill runtime environments with newer non-syntax parts of the JavaScript language that it may be lacking. Below is an example of an Object.assign() polyfill that will check to see if the JavaScript runtime has Object.assign() and if not will add it to the runtime.


// if Object.assign is missing then polyfill it.
if (typeof Object.assign != 'function') {
    // Must be writable: true, enumerable: false, configurable: true
    Object.defineProperty(Object, "assign", {
        value: function assign(target, varArgs) { // .length of function is 2
        'use strict';
        if (target == null) { // TypeError if undefined or null
            throw new TypeError('Cannot convert undefined or null to object');
        }

        var to = Object(target);

        for (var index = 1; index < arguments.length; index++) {
            var nextSource = arguments[index];

            if (nextSource != null) { // Skip over if undefined or null
            for (var nextKey in nextSource) {
                // Avoid bugs when hasOwnProperty is shadowed
                if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                to[nextKey] = nextSource[nextKey];
                }
            }
            }
        }
        return to;
        },
        writable: true,
        configurable: true
    });
}

Basically, JavaScript polyfills fill in newer missing API features if they are missing from the runtime. The babel-polyfill documentation offers the following explanation for polyfill'ing:

"You can use new built-ins like Promise or WeakMap, static methods like Array.from or Object.assign, instance methods like Array.prototype.includes, and generator functions (provided you use the regenerator plugin). The polyfill adds to the global scope as well as native prototypes like String in order to do this." - Babel Docs

Polyfills are typically used in conjunction with compiling tools like Babel to offer full compatibility for both syntax and newer API features (i.e. syntax meaning something like: () => {} and API meaning something like Object.assign()). This is why Babel provides the babel-polyfill.

Notes:

  1. The polyfills used by Babel can be also used as standalone solutions with Node and Web browsers (i.e. regenerator runtime and core-js).
  2. Tools like polyfill.io are available for polyfill'ing browsers with not just JavaScript API updates but also with browser/web API updates as well (e.g. requestAnimationFrame).

Chapter 3 : New ES2015+ Methods

In this chapter, I'll break down the newest methods from ES2015+. When targeting an ES5 only runtime (i.e. IE9) all of these methods have to be polyfilled.

3.1 : New Number Static Method (ES2015)

ES2015 added the Number.isInteger() method that will return true if the value passed to it is an integer (i.e. a number with no fractional part). Otherwise, it will return false.


console.log(Number.isInteger(0)); // logs true
console.log(Number.isInteger(1)); // logs true

console.log(Number.isInteger(0.1)); // logs false, has a fractional part
console.log(Number.isInteger(Infinity)); // logs false
console.log(Number.isInteger([1])); // logs false
    

3.2 : New String Methods (ES2015, ES2017)

  • ''.startsWith() ES2015
  • ''.endsWith() ES2015
  • ''.includes() ES2015
  • ''.repeat() ES2015
  • ''.padStart() ES2017
  • ''.padEnd() ES2017
  • ''.matchAll() (coming soon, stage 3 proposal)
  • ''.trimStart() / ''.trimLeft() (coming soon, stage 3 proposal)
  • ''.trimEnd() / ''.trimRight() (coming soon, stage 3 proposal)

ES2015 added ''.startsWith()and ''.endsWith(). These methods can check if a string begins or ends with a specific sub-string.


// True or false does 'pre-funded' start with 'pre-'
console.log('pre-funded'.startsWith('pre-')) // logs true

// True or false does 'pre-funded' end with 'funded'
console.log('pre-funded'.endsWith('funded')) // logs true
    

ES2015 added ''.includes(). This method will check if a string contains a specific sub-string.


// True or false does 'pre-funded' include the sub string 'fund'
console.log('pre-funded'.includes('fund')) // logs true
    

ES2015 added ''.repeat(). This method will take a string and return the same string repeated as many times as provided in the first argument.


// Take the string 'He' and return a string containing 'He' repeated three times
console.log('He'.repeat(3)) // logs 'HeHeHe'
    

ES2017 added ''.padStart() and ''.padEnd(). These methods will pad the beginning or end of a string with repeating non-breaking spaces (the default) or a specified string of characters. When padding you supply the final length of the entire string. Including the original string. The non-breaking spaces or specified string will fill in any of the characters not taken up by the original string.


// Pad the start of 'GO!' with spaces so the total length of the string is 10, including 'GO!'.
console.log('GO!'.padStart(10)) // logs '       GO!'
// note how the ' ' repeated until a length of 10 was reached

// Pad the start of 'GO!' with '.' so the total length of the string is 10, including 'GO!'.
console.log('GO!'.padStart(10, '.')) // logs '.......GO!'
// note how the '.' repeated until a length of 10 was reached

// Pad the end of 'GO!' with spaces so the total length of the string is 10, including 'GO!'.
console.log('GO!'.padEnd(10)) // logs 'GO!       '
// note how the ' ' repeated until a length of 10 was reached

// Pad the start of 'GO!' with '.' so the total length of the string is 10, including 'GO!'.
console.log('GO!'.padEnd(10, '!')) // logs 'GO!!!!!!!!'
// note how the '!' repeated until a length of 10 was reached
    

Notes:

  1. The string methods ''.matchAll(), ''.trimStart(); ''.trimLeft();, and ''.trimEnd(); ''.trimRight(); are currently at stage 3.

3.3 : New Array Static Methods (ES2015)

  • Array.from()
  • Array.of()

ES2015 added the Array.from() static method. This method will take Array-like values (objects with a length property and indexed values) or iterable values and convert them into Array values (iterable values are: String, Array, TypedArray, Map, and Set).


// Array from Array-like values
const myArray1 = Array.from({length: 2, 0: 'zero', 1:'one'});
console.log(myArray1); // logs ["zero", "one"]

// Array from String (i.e. an iterable)
const myArray2 = Array.from('foo');
console.log(myArray2); // logs ["f", "o", "o"]

// Array from an Array (i.e. an iterable)
const myArray3 = Array.from([1, 2, 3])
console.log(myArray3); // logs [1, 2, 3], well that is silly

// Array from an Array (i.e. an iterable), where each item in the array is run through a function
// The second argument to .from() can be a function called on each iterable
const myArray4 = Array.from([1, 2, 3], item => item * item)
console.log(myArray4); // logs [1, 4, 9]
                

Notes:

  1. Array.from() creates a new, shallow-copied Array.

ES2015 added the Array.of() static method. This method creates an Array from arguments. Unlike the array constructor it can handle a case like Array.of(5) resulting in [5] while Array(5) will result in [undefined, undefined, undefined, undefined, undefined].


// create an array containing 5 values
console.log(Array.of(5,{},undefined,[],'string'));
/* logs:

[5, [object Object] { ... }, undefined, [], "string"]

*/

// create an array containing 5 undefined values
console.log(Array(5)); // works if only one argument is passed
/* logs:

[undefined, undefined, undefined, undefined, undefined]

*/

// create an array containing 2 numeric values, 5 and 4
console.log(Array(5,4));
/* logs:

[5, 4]

*/
                

3.4 : New Array Methods (ES2015, ES2016)

  • [].findIndex()
  • [].find()
  • [].includes()
  • [].keys()
  • [].values()
  • [].entries()
  • [].copyWithin()
  • [].fill()
  • [].flat() (coming soon, stage 3 proposal)
  • [].flatMap() (coming soon, stage 3 proposal)

ES2015/ES2016 added the [].findIndex(), [].find() and [].includes() methods. The [].find() method is used to find a specific value in an array and return that value. [].findIndex() is used to find a specific value and return its index in the array. Both use a testing function to iterate over the Array and return the first truthy value returned from the testing function. The [].includes() method is used to verify (true or false) if an array contains a specific value.


const myArray = [10, 20, 30, 40];

// find and return the first value in the array that is greater than 20
console.log(myArray.find(function(item){ return item > 20})) // logs 30

// find and return the index of the first value in the array that is greater than 20
console.log(myArray.findIndex(function(item){ return item > 20})) // logs 2

// does myArray contain the value 30
console.log(myArray.includes(30)) // logs true
                

ES2015 added the [].keys(), [].values(), and [].entries() methods. The [].keys() method returns the keys from an Array as an Array iterator object. The [].values() method returns the values (i.e. the items) from an Array as an Array iterator object. And the [].entries() returns an Array iterator containing each item as a key-value array (i.e. [[0, item0], [1, item1]]).


let myArray = ['item0', 'item1', 'item2'];

// All of these log "[object Array Iterator] "
console.log(myArray.keys().toString());
console.log(myArray.values().toString());
console.log(myArray.entries().toString());

// create an iterator containing the index's of an array (i.e. the key's)
let myArrayKeys = myArray.keys();
console.log(myArrayKeys.next().value) // logs 0
console.log(myArrayKeys.next().value) // logs 1
console.log(myArrayKeys.next().value) // logs 2

// create an iterator containing the values from an array
let myArrayValues = myArray.values();
console.log(myArrayValues.next().value) // logs "item0 "
console.log(myArrayValues.next().value) // logs "item1 "
console.log(myArrayValues.next().value) // logs "item2 "

// create an iterator containing both the keys and the values, inside of an array
let myArrayEntries = myArray.entries();
console.log(myArrayEntries.next().value) // logs [0, "item0"]
console.log(myArrayEntries.next().value) // logs [1, "item1"]
console.log(myArrayEntries.next().value) // logs [2, "item2"]
                

Notes:

  1. The [].keys(), [].values(), and [].entries() array methods are very similar to the [].keys(), [].values(), and [].entries() found on the Map() and Set() values.
  2. An iterator is basically an object with a .next() method.
  3. Don't confuse an iterator with an iterable. An iterator will keep track of what comes next (i.e. .next()) while a iterable value is simply a value that comes with an interface for iterating over the value. An array is by default an iterable. But, methods like [].keys(), [].values(), and [].entries() can be used to create an Array iterator object around the original array that will keep track of the current iteration and knows what the next item in the iteration is and how to get it.

ES2015 added the [].copyWithin() method. This method shallow copies a range of items in an Array and then inserts the copies back into the same Array replacing items in the Array starting at a specific index.


let myArray1 = [1,1,1,1,5,5,5,5];
// copy from index 4 to 6 and take the copies and start replacing at index 0
console.log(myArray1.copyWithin(0, 4, 6)); //logs [5, 5, 1, 1, 5, 5, 5, 5]
// i.e. this will replace the values at index 0 and index 1 with copied values 5, 5

let myArray2 = [1,1,1,1,5,5,5,5];
// passing negative numbers to any of the parameters means start from the end of the array
// copy from -4 index to -2 and take the copies and start replacing at index 2
console.log(myArray2.copyWithin(-6, -4, -2)); //logs [1, 1, 5, 5, 5, 5, 5, 5]
// i.e. this will replace the values at index 2 and index 3 with copied values 5, 5

let myArray3 = [1,1,1,1,5,5,5,5];
// copy from index 4 to end and take the copies and start replacing at index 0
console.log(myArray3.copyWithin(0, 4)); //logs [5, 5, 5, 5, 5, 5, 5, 5]
// i.e. this will replace the values at index 0, 1, 2, 3 with copied values 5, 5, 5, 5
                

Notes:

  1. The second and third arguments passed to [].copyWithin() will accept negative numbers indicating a range which starts or counts from the end not the beginning.

ES2015 added the [].fill() method. This method will replace or fill a range of items in an Array with a new value.


// replace the values at index 0 and index 1 with 'foo'
// the second and third arguments to fill() below say, start filling at index 0 and stop at index 2
console.log([5,5,5,5].fill('foo',0,2)) // logs ["foo", "foo", 5, 5]

// replace all values from index 2 on with 'foo'
console.log([5,5,5,5].fill('foo',2)) // logs [5, 5, "foo", "foo"]

// replace all values with 'foo'
console.log(Array(4).fill('foo')) // logs ["foo", "foo", "foo", "foo"]
                

Notes:

  1. The second and third arguments passed to [].fill() will accept negative numbers indicating a range which starts or counts from the end not the beginning.

3.5 : New Object Static Methods (ES2015, ES2017)

  • Object.is()
  • Object.values()
  • Object.entries()
  • Object.assign()
  • Object.fromEntries() (coming soon, stage 3 proposal)

ES2015 added the Object.is() static method. This method accepts two values and if the values are the same then it returns true, otherwise, it returns false.


// compare the same Object object
const myObject = {};
console.log(Object.is(myObject,myObject)); // logs true, same

// compare different Object objects
console.log(Object.is({},{})); // logs false, two different objects

// compare primitive values
const myNumber = 5;
const myBoolean = true;
const myString = '';
const myNull = null;
const myUndefined = undefined;
console.log(Object.is(5,myNumber)); // logs true, same value
console.log(Object.is(true,myBoolean)); // logs true, same value
console.log(Object.is('',myString)); // logs true, same value
console.log(Object.is(null,myNull)); // logs true, same value
console.log(Object.is(undefined,myUndefined)); // logs true, same value
                

Notes:

  1. Object.is(1,1) is the same as 1 === 1, except for the following cases, Object.is( +0, -0 ) is false, while -0 === +0 is true and Object.is( NaN, NaN ) is true, while NaN === NaN is false.

ES2017 added the Object.values() static method. This method will return an Array of enumerable property values from an Object.


// log the values in myObject
var myObject = { 0: 'f', 1: 'o', 2: 'o' };
console.log(Object.values(myObject)); // logs ['f', 'o', 'o']

// the string primitive value will be coerced to an object if passed to Object.values();
console.log(Object.values('foo')); // logs ['f', 'o', 'o']
                

ES2017 added the Object.entries() static method. This method will return an objects properties, as key-value pairs inside an Array (e.g. [property, value]) inside a single wrapping Array (e.g. a multidimensional array [[property, value], [property, value]]).


// log the key and value pairs in myObject
var myObject = { 0: 'f', 1: 'o', 2: 'o' };
console.log(Object.entries(myObject)); // logs [["0", "f"], ["1", "o"], ["2", "o"]]
                

Notes:

  1. Don't forget Object.keys() was added in ES5.

ES2015 added the Object.assign() static method. This method copies own enumerable properties from one object to a different target object.


// using assign() to clone an object
const myObject = {'key1':'value1', 'key2':'value2'};
console.log(Object.assign({}, myObject));
/* logs new object, cloned from myObject

[object Object] {
    key1: "value1",
    key2: "value2"
}

*/

// using assign() to merge objects
const myObject1 = {'key1':'value1', 'key2':'value2'};
const myObject2 = {'key3':'value3', 'key4':'value4'};
const myObject3 = {'key4':'4'};
console.log(Object.assign(myObject1, myObject2, myObject3));
/* logs a new object

[object Object] {
    key1: "value1",
    key2: "value2",
    key3: "value3",
    key4: "4"
}

Note: same properties are overwritten. Last in to the parameter list wins.

*/

// using assign to coerce a string to an object
console.log(Object.assign({},'foo'));

/* logs a new object

[object Object] {
    0: "f",
    1: "o",
    2: "o"
}

*/
                

Notes:

  1. The Object.assign() won't deep clone reference values; it will only deep clone the reference, not the value. This means that if you are cloning an object with reference values (e.g. objects inside of objects) the reference value is not cloned just the reference. Thus, deep cloning using Object.assign() copies pointers (i.e. the reference), not values. If you need to deep clone an object, you'll have to resort to other methods.
  2. The Object.assign() is commonly used to merge objects together or create shallow clones of objects.

Chapter 4 : New ES2015+ Syntax

In this chapter, I'll break down the most used syntax updates from ES2015 and beyond. When targeting an ES5 only runtime these syntax updates have to be compiled from ES2015+ to ES5 (e.g., Babel > ES5).

4.1 : Using const and let (ES2015)

Before ES2015 variables were declared using the var keyword. Today, it is more common that developers completely avoid the use of var and instead use const and let when needed. Const is used to declare variables with values that do not get reassigned. Reassigning a const value throws an error. Additionally, both const and let honor all block scope (i.e. { block scope }), unlike var which only honors function block scope (i.e. function myFunction(){ block scoped }).

This means const and let are safer because they don't leak out of things like if blocks or looping blocks. For example, in the code below the variables, doug and MATH are scoped to the if block while jill leaks out.


// MATH_CONSTANT and doug are scoped within the if(){ ... } blocks and unlike var they will not leak out.
if(true){
    let doug = 45;
    const MATH_CONSTANT = Math.PI;
    var jill = 44; // does not care about brackets
}

console.log(jill); // logs 44, because it leaked out of { }

//

try {
    console.log(doug);
    console.log(MATH_CONSTANT);
}catch(e){
    console.log('Can\'t find doug or MATH_CONSTANT');
}
    

Notes:

  1. Keep in mind, using const does not create an immutable value, it just means that the variable can't be reassigned. Thus, changing the properties in something like an Object or the values in an Array that has been assigned to a const will not throw an error because it is not being reassigned (i.e. the reference/pointer to the value did not change thus no error).

Today expect to see code that never uses var, favors const, and uses let only when re-assignment is needed (i.e. let score = 0; score = 1; v.s. const pie = 3.14;).

4.2 : Using blocks to Create Scope (ES2015)

Before ES2015 if one needed a unique scope the only option was to use function scope:


// below doug and MATH_CONSTANT are only available with the scope of the function
(function () {
    var doug = 45;
    var MATH_CONSTANT = Math.PI
}());

try {
    console.log(doug);
    console.log(MATH_CONSTANT);
}catch(e){
    console.log('Can\'t find doug or MATH_CONSTANT');
}
    

Today, given that const and let remain scoped to blocks with no leaking developers can replace IIFE with blocks if var is avoided.

So, don't be surprised if you see IIFE's replaced with simple blocks:


// doug and MATH_CONSTANT only available with the scope of the blocks
{
    let doug = 45;
    const MATH_CONSTANT = Math.PI;
}

try {
    console.log(doug);
    console.log(MATH_CONSTANT);
}catch(e){
    console.log('Can\'t find doug or MATH_CONSTANT');
}
    

Notes:

  1. Keep in mind that the need to construct a private scope is being accomplished today with ES2015 modules. Modules have their own module scope by default. In other words, in modern code, ECMAScript modules are already scoped privately simply by creating the file. One does not need to create another private scope on top of that.

4.3 : Using Default Function Parameters (ES2015)

In the past, if a default value was needed for an argument passed to a function, boilerplate was needed:


const add = function(x, y) {
    x = x || 0;
    y = y || 0;
    return x + y;
}

console.log(add()) //logs 0
    

No longer is this the case. It is now possible to give parameters default values when defining a function. The code below is equivalent to the previous code.


const add = function(x = 0, y = 0) {
    return x + y;
}

console.log(add()) //logs 0

Notes:

  1. Parameter default values are triggered by undefined, not simply a falsely value like null or ''.
  2. The arguments array, available within the scope of a function, is not affected by default parameters values in any way.

4.4 : Using Destructuring Assignments (ES2015)

Destructuring is a fancy word for unpacking elements from an array, or characters from a string, or properties from an object and assigning/reassigning them to one or more variables in a terse expression. Below are three code examples of each of these destructuring expressions just mentioned.

1. Destructuring Strings:


// destructuring Strings characters into the variables a, b, and c
let [a, b, c] = "foo";
console.log(a); // logs f
console.log(b); // logs o
console.log(c); // logs o

/* Could be written
let a;
let b;
let c;
[a, b, c] = "foo"; // i.e. a = 'f', b='o', c='o'
console.log(a); // logs f
console.log(b); // logs o
console.log(c); // logs o
*/
    

2. Destructuring Arrays:


// destructuring an Array of elements in the variables one, two, and three
let [one, two, three] = [1,2,3];
console.log(one); // 1
console.log(two); // 2
console.log(three); // 3
    

3. Destructuring Objects:


// destructuring an Object properties in the variables f and l
// i.e. find the property first, assign its value to f. Find property last, assign its value to l.
let { first: f, last: l } = { first: 'Bill', last: 'May' };
console.log(f); // Bill
console.log(l); // May
// The object is used to identify the property and new variable that will hold the properties value

// Note the above code is commonly written using shorthand properties names
let { first, last } = { first: 'Bill', last: 'May' };
console.log(first); // Bill
console.log(last); // May
// This means you omit the : f and : l, and the assignment uses first:first and last:last
// But you don't have to write first:first or last:last, just first and last

/* the above is just shorthand for:

let { first:first, last:last } = { first: 'Bill', last: 'May' };

*/
    

Don't over think destructuring, it simply is a terse way to take a collection of values and assign those values within the collection to different or new variables.

Notes:

  1. Parentheses are required around object destructuring when the expression is assigning values (i.e. {a, b} = {a: 1, b: 2}; will throw an error but ({a, b} = {a: 1, b: 2}); will not).

4.5 : Using Destructuring With Function Arguments and Parameters (ES2015)

Take what you know about destructuring and now apply it to function arguments and parameters. Basically, a function parameter can be written as a collection of identifiers that can destructure String, Array, and Object values. The result is an extremely terse way to unpack Arrays, Objects, or Strings into separate parameters using a single argument.

Array arguments can be destructured:


// assign argument values to identifiers in the first parameter
let func = function([one, two, three]) {
    console.log(one, two, three);
}
func([1,2,3]);
// Same concept as: let [one, two, three] = [1,2,3]; but using arguments & parameters instead
    

Object arguments can be destructured (Most Common Usage):


// assign argument values to identifiers in the first parameter
let func = function({ first: f, last: l }) {
    console.log(f,l);
}
func({ first: 'Bill', last: 'May' });
// Same concept as: let { first: f, last: l } = { first: 'Bill', last: 'May' };
    

String arguments can be destructured:


// assign argument values to identifiers in the first parameter
let func = function([a, b, c]) {
    console.log(a,b,c);
}
func('foo');
// Same concept as: let [a, b, c] = "foo";
    

Destructuring function parameters are a terse way to pass complex data via a single argument and have the argument values be available in the function for immediate use without creating any assignment boilerplate in the function.

Notes:

  1. Destructuring can occur on any parameter.

4.6 : Using Default Destructuring Values (ES2015)

Destructuring syntax also permits the use of default values so that assignments can have fallback values. In other words if the value you are destructuring is undefined a fall back value can be setup.


// Note: Fallback to default value when destructuring Strings
const [a='f', b='o', c='o'] = '';
console.log(a); // log f
console.log(b); // log o
console.log(c); // log o

// Note: Fallback to default value when destructuring Arrays
const [one=1, two=2, three=3] = [undefined,44,undefined];
console.log(one); // log 1
console.log(two); // log 44
console.log(three); // log 3

// Note: Fallback to default value when destructuring Objects
const { first: f = 'John', last: l = 'Doe' } = { first: 'Mary' };
console.log(f); // log Mary
console.log(l); // log Doe
    

This will of course also work with when destructuring function arguments


// destructuring array argument, with default values
const func1 = function([one=1, two=2, three=3]) {
    console.log(one, two, three);
}
func1([]);

// destructuring object argument, with default values
const func2 = function({ first: f = 'John', last: l = 'Doe' }) {
    console.log(f,l);
}
func2({});

// destructuring string argument, with default values
const func3 = function([a = 'f', b = 'o', c = 'o']) {
    console.log(a,b,c);
}
func3('');

4.7 : Using the Spread Operator (ES2015, ES2018)

The spread operator (i.e. ...) is used to unpack or expand elements from an Array, properties from an Object, or individual characters from a String, so as to immediately use these individual values in a couple of specific cases. Below I detail these cases.

1. Arrays can be spread into Array literals or when calling a function:


// spreading an Array into a function call
console.log(...['f', 'o', 'o']); // logs f o o,  like calling console.log('f', 'o', 'o');


// spreading Array(s) into an Array literal
console.log([...[1, 2], ...[3, 4, 5]]); // logs [1,2,3,4,5]

    

Notes:

  1. Spreading Arrays into Array literals is commonly used to clone Arrays, merge Arrays into a new or old Arrays, and spread Array elements into a function call as arguments. Spreading an Array can replaces usages of .concat().

2. Object properties can be spread into Object literals:



const obj1 = {bob: true, steve: false};
const obj2 = {lisa: true, bob: false};

// spreading Objects into an object literal
console.log({...obj1, ...obj2}); // logs {bob: false, steve: false, lisa: true}
// Note that last bob value in wins.

    

Notes:

  1. Spreading objects was added in ES2018.
  2. Order matters, last property and value spread wins.
  3. Spreading Objects into object literals is commonly used to clone Objects, merge objects into a new or old Objects, or filling in defaults in Objects. Spreading an Object can replace usages of Object.assign({}, obj1, obj2);.

3. Strings can be spread into Array literals or when calling a function:


// spreading a string into an array
console.log([...'bar']);
// logs ['b','a','r']

// spreading a String into a function call
console.log(...'foo');
// i.e. console.log('f','o','o');
// logs f o o

    

4.8 : Using the Rest Operator (ES2015, ES2018)

Unfortunately, the rest operator (i.e. ...) looks exactly like the spread operator but instead of expanding things it will wrap things up.

The rest operator can be used in the following two cases:

1. When defining function parameters a rest operator can be used on the last parameter to indicate that any unidentified arguments should be wrapped up into an array.


/*
By place the ... operator directly in front of the last function parameter the rest
of the arguments passed to a function besides the ones before the rest parameter
are wrapped up into an array.
*/
const func = function(param1, param2, ...restOfArguments) {
    console.log(param1, param2, restOfArguments);
}
// call func with 7 parameters, but only two are define, the rest are wrapped up into an array
func(1,2,3,4,5,6,7); // logs 1 2 [3, 4, 5, 6, 7]
    

2. When destructuring, remaining values can be wrapped up too:


// destructuring String characters in the variables a, b, c, and the rest into d Array
const [a, b, c, ...d] = "doggy";
console.log(a); // d
console.log(b); // o
console.log(c); // o
console.log(d); // ["g", "y"]
// Note: use of Array of variables before assignment

// // logs f o o  an Array elements in the variables one, two, three, and the rest in restOfNumbers of Array
const [one, two, three, ...restOfNumbers] = [1, 2, 3, 4, 5, 6];
console.log(one); // 1
console.log(two); // 2
console.log(three); // 3
console.log(restOfNumbers); // [4, 5, 6]

// // logs f o o  an Object properties in the variables f, l, and the rest in restOfProps Object
const { first: f, last: l, ...restOfProps } = { first: 'Bill', last: 'May', age: 45, living: false };
console.log(f); // Bill
console.log(l); // May
console.log(restOfProps); // { age: 45, living: false }
    

Careful not to confuse the spread operator with the rest operator. The ... operator is identical but the context in which it is used changes how the operator functions. The rest operator is used when defining function parameters and destructuring. The spread operator turns iterable items into arguments for functions, into properties for Object literals, or elements for Array literals.

4.9 : Using Template Literals (ES2015, ES2016, ES2018)

Characters wrapped in backticks (i.e. `string here`) instead of quotes are considered template string literals that return a String value. Template literals support line breaks and interpolation (i.e. a template like syntax for easily inserting values into the string, using ${}).

In the code example below a multi-line string is created from the firstName and lastName variables


let firstName = 'Jane';
let lastName = 'Smith';

console.log(`Hello Mr. ${lastName}!
Welcome!
May I call you ${firstName}?`);

// Note: when log to the console line breaks are honored

/*
"Hello Mr. Smith!
Welcome!
May I call you Jane?"
*/
    

Notes:

  1. A backslash is used for escaping inside template literals (e.g. `\${}` becomes '${}'.

4.10 : Using Tagged Template Literals (ES2015)

Tagged template literals are template literals that get run through a function and passed the template literal details. Commonly a String value is returned from a tag function, but any value can be returned.

In the code example below the tag function verifyName will use a default name if one of the values passed to the template literal is undefined. Basically, by tagging the template literal with a tag function I make sure it will always have a first and last name even if the values passed to it are undefined.


// This is a contrived example highlighting the mechanics of a tag function

let name;

// tag function to adjust string if name is undefined
const verifyName = function(stringsFromTemplate, nameTemplateData) {
    const s = stringsFromTemplate; // In an array

    if(nameTemplateData === undefined){
        return `${s[0]}${'[no name given]'}${s[1]}${'[no name given]'}${s[2]}`
    }else{
        return `${s[0]}${nameTemplateData}${s[1]}${nameTemplateData}${s[2]}`
    }
}

// Use verifyName tagged function with undefined value
let greeting1 = verifyName`Hello ${name}! May I call you ${name}?`
console.log(greeting1); //logs Hello [no name given]! May I call you [no name given]?

name = 'Pat';

// Use verifyName tagged function with defined value
let greeting2 = verifyName`Hello ${name}! May I call you ${name}?`
console.log(greeting2); //logs Hello Pat! May I call you Pat?
    

Notes:

  1. JavaScript provides one tag function baked into the language String.Raw(). This tagged function ignores backslashes and returns the raw characters contained in the template literal (e.g. String.raw`\${}` returns '\${}').

4.11 : Using Fat Arrow Function Expressions (ES2015)

Today, developers have replaced traditional function expressions like:


const func = function (x) { return x * x; };
        

with a new syntax that uses a fat arrow (i.e. =>):


const func = x => x * x ;
// note: Omitted parenthesis around single parameter and use of implicit return.
// This works fine when you have a single parameter and a single expression to return.

// a less terse version of the above
const func = (x) => { return x * x; };

// parenthesis and blocks only required if you have more than one parameter or expression
const func = (x, y) => {
    const doubleY = y * 2;
    return z * doubleY;
};
        

The previous code examples are similar in that they are function expressions, but the fat arrow function is not a total replacement for functions in general. Fat arrow function expressions are best suited for non-method functions, and they cannot be used as constructor functions (Arrow functions do not have prototypes). In most cases, fat arrow function expressions are used due to their terseness.

Fat arrow functions can also provide another benefit besides terseness. They will take on the value of this in the context they are used instead of binding a new this value (i.e. fat error functions provide a lexical this, meaning the this value is determined based on surrounding scope at runtime).

Have you ever had to hack a reference to this in the function scope chain using a that variable:


// Broken because using this keyword creates a new binding in function(){}

var CounterBroken = function () {
    this.num = 0;
    this.timer = setTimeout(function () {
        // console.log(this); //this, refers to window
        this.num++; // this, does not refer to CounterBroken instance
        console.log(`Broken this = ${this.num}`); //logs Broken this = NaN
    }, 1000);
}

var counterBrokenInstance = new CounterBroken(); // Create an Instance of CounterBroken

// Fixing broken counter constructor using that = this

var CounterFixed = function () {
    var that = this;
    this.num = 0;
    this.timer = setTimeout(function() {
        console.log(that.num); //that = this, from scope above
        that.num++; // this will now work, but a scope chain hack
        console.log(`Fixed this = ${that.num}`); //logs Fixed this = 1
    }, 1000);
}

var counterInstance = new CounterFixed(); // Create an Instance of CounterFixed

Using the new fat arrow function you no longer have to write var that = this or perform binding()'s. The fat arrow function provides this by default (i.e. lexically scoped this):


// Remove that = this, just use fat arrow function expression instead

var CounterFixed = function () {
    this.num = 0;
    this.timer = setTimeout(() => {
        console.log(this.num); // this, is CounterFixed instance
        this.num++; // this refers to what we want now with no hack
        console.log(`Fixed this = ${this.num}`);
    }, 1000);
}

var counterInstance = new CounterFixed();

Notes:

  1. "An Arrow Function does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment. " - http://www.ecma-international.org/ecma-262/6.0/#sec-arrow-function-definitions-runtime-semantics-evaluation
  2. Forging the use of the blocks in an fat arrow function expression can require the use of parentheses. (e.g. const func = () => foo(); should be written const func = () => (foo()); and const obj = x => { bar: x }; should be written const obj = x => ({ bar: x });.
  3. The return keyword is implied and can be omitted when using an Arrow function (e.g. let add = (x,y) => x + y)

4.12 : Using Trailing Commas (ES2015, ES2017)

Trailing commas in the code below are considered valid JavaScript today:


//Trailing comma in Array literal
console.log([1, 2, 3,]) // no error

//Trailing comma in Object literal
console.log({bob: true, steve: false,}) // no error

//Trailing comma in Function parameter definitions
const func1 = ((x,) => { // no error
    console.log(x)
})('cat');

//Trailing comma in Function arguments call
const func2 = ((x) => {
    console.log(x)
})('dog',); // no error

// Trailing comma in array destructuring
const [index0, index1,] = ['bee', 'flee']; // no error
console.log(index0, index1);

// Trailing comma in object destructuring
const {bob:bobsValue, jill:jillsValue,} = {bob: false, jill: true}; // no error
console.log(bobsValue,jillsValue);
            

Notes:

  1. Trailing commas are not allowed in JSON.
  2. Pragmatically trailing commas can be useful because you don't have to adjust commas anymore when adding new elements, parameters, or properties to JavaScript code. Additionally, not throwing an error on a trailing commas makes version control diffs cleaner and less troublesome.
  3. Values using the rest operator may not have a trailing comma e.g. :

    const [a, ...b,] = [1, 2, 3];

    and

    const func = (...p,) => {};

    this will cause a SyntaxError.

4.13 : Using the for-of loop (ES2015)

Commonly looping today will be done by way of an Array method like .map() or .forEach(). However, don't be surprised if you see the for-of loop in modern code. It can be used to loop over Strings, Arrays, Maps, Sets, basically anything that is an iterable value.

The for-of loop goes through an iterable and assigns each entry in the iterable one at a time to the variable(s) define before the of keyword.

Looping over Arrays using for-of loop:


const anArray = ['a', 'b', 'c'];
for (const item of anArray) {
    console.log(item); // logs "a", "b", "c "
}
    

Looping over Strings using for-of loop:


const aString = 'cat';
for (const character of aString) {
    console.log(character); // logs "c", "a", "t "
}
    

Looping over Maps using for-of loop:


// create a new Map called myMap and preload it with key-value entries
const myMap = new Map([
    ['key1', 'value1'],
    ['key2', 'value2']
]);

// Use for-of loop to loop over Map
for (const entry of myMap){
    console.log(entry);
    // logs:
    // ["key1", "value1"]
    // ["key2", "value2"]
}
    

As of ES2017 one can use Object.entries() and destructuring with the for-of loop to loop over Object literals.:


const arr = ['zero', 'one'];

for (const [index, element] of arr.entries()) {
    console.log(index, element);
}

// above logs 0 "zero"
// above logs 1 "one"

Notes:

  1. The for-of loop provides similar terseness found with Array looping methods but also supports break and continue.

4.14 : Using Shorthand Property Names (ES2015)

When defining an object literal if the colon and value are omitted the value from a similarly named variable within the same scope will be used. In the code below the const myNumber value is used for the myNumber property value in the myObjectLiteral object.


const myNumber = 1;

const myObjectLiteral = {
    myNumber, // notice all I put here was a property name
    // the above is shorthand for myNumber : myNumber
};

console.log(myObjectLiteral); // Logs Object {myNumber: 1}
    

Notes:

  1. Shorthand property names are often used in combination with destructuring.

4.15 : Using Shorthand Method Names (ES2015)

When adding methods (i.e. functions) to object literals a new terser syntax is possible.

This is the old non-short syntax:


const myObjectLiteral = {
    myMethod: function(parameters){
        return;
    }
};
    

This is the new shorthand syntax.


const myObjectLiteral = {
    myMethod(parameters){
        return;
    }
};
    

Which saves you from having to write, : function.

Note how similar this new syntax is to getters and setters from ES5:


const myObjectLiteralWithGetterAndSetter = {
    set myMethod(parameters){
      return;
    },

    get myMethod(){
      return;
    },
};
    

4.16 : Using Computed Property Names (ES2015)

Using brackets around property names/keys will permit the name/key to be a result of an expression.


const prop = 'prop';

// define an object literal with name/keys that are the result of an experssion
const myObject = {
    // note property keys/names are computed
    [prop + 1]: 1,
    [prop + '2']: 2
}

console.log(myObject);

/* logs

[object Object] {
    prop1: 1,
    prop2: 2
}

*/
    

Notes:

  1. This is similar to the bracket notation that can be used to access a key/property on an object from a computed property name.

Chapter 5 : ES2015 Map() and Set()

This chapter will cover usages for the new Map() and Set() objects.

5.1 : Using Maps instead of Object Literals (ES2015)

ES2015 comes with Map(). Maps are key-value pairs stored in insertion order. Sounds a lot like an object right (i.e. {key1:value1, key2:value2})? Think of Maps as objects but with a built-in native API for working with the object and its key-value pairs.

Examine the code below to gain a basic overview of the creation and usage of a Map. Note that most of the built in methods for Map() are demonstrated (e.g. clear(), entries(), forEach(), get(), has(), keys(), set(), values()).


// create a new Map called myMap
// Preload myMap with key-value entries, as an Array, contained in an Array
const myMap = new Map([
    ['key1', 'value1'],
    ['key2', 'value2']
]);

// .forEach()
// loop over each entry in the Map using Map's forEach() method
myMap.forEach((value, key) => {
    console.log(`${key} = ${value}`);
}); // logs "key1 = value1 " "key2 = value2 "

// .entries()
// List of all key-value pairs, spreading into console log
console.log(...myMap.entries()) // logs ["key1", "value1"] ["key2", "value2"]

// .keys()
// List of all keys, spreading into console log
console.log(...myMap.keys()) // logs "key1" "key2 "

// .values()
// List of all values, spreading into console log
console.log(...myMap.values()) // logs "value1" "value2 "

// .set()
// Set a key-value pair
myMap.set('key3', 'value3');

// .get()
// Get a key-value pair
console.log(myMap.get('key3')); // logs "value3"

// .has()
// Does a key have a value yet
console.log(myMap.has('key3')); // logs true

// .delete()
//Delete a key-value pair, by key
myMap.delete('key3')
console.log(myMap.has('key3')) // logs false

// .clear()
//Clear all key-value pairs
myMap.clear();

// .size()
//Get current size of map (i.e. number of entires)
console.log(myMap.size) // logs 0, because you just cleared the Map of entries
    

Don't forget when using a Map() the key can be any value:


const myMap = new Map([
    ['key1', 'some value for key 1'],
    [2, 2],
    [[1], [1,2,3]],
    [{id:1}, {prop1:'value',prop2:'value'}],
]);

myMap.forEach( (value, key) => {

    console.log(`${key} = ${value}`);
    // logs:
    //
    // "key1=s ome value for key 1"
    // "2=2"
    // "1=1,2,3"
    // "[object Object]=[ object Object]"

});
    

Notes:

  1. Until ES2015 Object literals we're the substitute for a Map. Obviously a real Map provides more features and out of the box methods for working with key-value pairs.
  2. Maps can be looped over using the for-of loop.
  3. The key and the value can both be any type of value, unlike Object literals in which the key is typically a string.
  4. When should you use Maps over object literals? Use a Map when you are performing a lot of work on the entries and the built in methods make everything simpler (clear(), entries(), forEach(), get(), has(), keys(), set(), values()).
  5. The .set() method returns the Map. Thus, .set() can be chained (i.e. myMap.set(key,value).add(key,value);).
  6. If you want to use Array methods on a Map simply spread the map into an array (i.e. [...myMap].filter(...);).
  7. A narrow version of Map() is also available called weakMap(). A weakMap(), mainly differs from a Map() in that it can only hold values that are objects and when a value is removed from a weakMap(), and it has no other references, the value will immediately be garbage collected (aka weak references). Additionally a weakMap() can't be iterated over, uses .length to get size of the map, and only has set(), delete(), get() , and has() methods.

5.2 : Using Sets to create Arrays, with no Duplicates (ES2015)

ES2015 comes with Set(). Sets are values stored in a specific order that can't contain a duplicate. Sounds a bit like an Array object right (i.e. [value, value])? Think of Sets as Arrays but with a built in API for working with the items in the Array and the bonus of automatically eliminating duplicates.

Examine the code below to gain a basic overview of the creation and usage of a Set. Note that most of the built in methods for Set() are demonstrated (e.g. clear(), entries(), forEach(), has(), keys(), add(), values()).


// create a new Set called mySet and preload it with values using an array/iterable
// Note: that duplicates are removed when creating new Sets and adding values
const mySet = new Set(['one', 'two', 'three', 'three']);

// .forEach()
// loop over each value in the Set using Set's forEach() method
mySet.forEach((value) => console.log(value)); // logs "one" "two" "three"

// .entries()
// Note this uses each value as both the key and the value.
// List of all value entries, spreading into console log
console.log(...mySet.entries()) // logs ["one", "one"] ["two", "two"] etc...

// .keys()
// List of all keys (same func as values()), spreading into console log
console.log(...mySet.keys()) // logs "one" "two" "three"

// .values()
// List of all values, spreading into console log
console.log(...mySet.values()) // logs "one" "two" "three"

// .has()
// Does a Set have a specific value
console.log(mySet.has('three')); // logs true

// .add()
// Add 'four' and 'three' to the Set.
console.log(...mySet.add('three')); // logs "one" "two" "three"
console.log(...mySet.add('four')); // logs "one" "two" "three" "four"
// Note adding 'three' to a Set will not create a duplicate, as 'three' is a duplicate

// .delete()
//Delete a value
mySet.delete('four')
console.log(mySet.has('four')) // logs false

// .clear()
//Clear all ordered values
mySet.clear();

// .size()
//Get current size of Set (i.e. number of values)
console.log(mySet.size) // logs 0, because you just cleared the Set of values
        

Notes:

  1. The .add() method returns the Set. Thus, .add() can be chained (i.e. mySet.add('one').add('two');).
  2. If you want to use Array methods on a Set simply spread the set into an array (i.e. [...mySet].filter(...);).
  3. A narrow version of Set() is also available called weakSet(). A weakSet(), mainly differs from a Set() in that it can only hold values that are objects and when a value is removed from a weakSet(), and it has no other references, the value will be garbage collected (aka weak references). Additionally a weakSet() can't be iterated over, uses .length to get size of the set, and only has add(), delete(), and has() methods.

Chapter 6 : ES2015 Class Syntax

This chapter will discuss the ES2015 class syntactical sugar which conceals JavaScripts clunky object inheritance model.

6.1 : What class Syntax Conceals

ES2105 class syntax is a cleaner way to work with prototypal inheritance and object factories. The class, extend, constructor, super, and static keywords simplify and conceal what was already possible with JavaScript.

Before class syntax was available working with object constructors and prototypal inheritance, to mimic class's from other OOP languages, was done in the following way:


// Create a Human class, i.e. an object factory for Human instances
function Human(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

// Instances created from Human constructor have a fullName method
Human.prototype.fullName = function () {
    return `${this.firstName} ${this.lastName}`;
};

// Create a Developer class
function Developer(firstName, lastName, type) {
    Human.call(this, firstName, lastName);
    this.type = type;
}

// Have Developer inherit from Human, by creating an object that inherits from Humans prototype
Developer.prototype = Object.create(Human.prototype);

// Make the constructor property point at Developer, not Human
Developer.prototype.constructor = Developer;

// Instances created from Developer constructor have a fullNameAndLanguage method
Developer.prototype.fullNameAndLanguage = function () {
    return `${Human.prototype.fullName.call(this)} develops ${this.type}`;
};

// Add static helper function to Developer
Developer.isJSdev = function(cody){
    return cody.language.toLowerCase() === 'javascript';
};

// Create an instance of Developer
const cody = new Developer('Cody', 'Lindley', 'JavaScript');

// Call fullNameAndLanguage() method
console.log(cody.fullNameAndLanguage()); // logs "Cody Lindley develops JavaScript"

// Call fullName() method
console.log(cody.fullName()); // logs "Cody Lindley"

// Call static type of Developer
console.log(Developer.isJSdev(cody)); // logs true

Using ES2015 class syntax the above can be re-written like so:


// Create a Human class, i.e. an object factory
class Human {
    constructor (firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    // Instances created from Human constructor have a fullName method
    fullName(){
        return `${this.firstName} ${this.lastName}`;
    }
}

// Create a Developer class
class Developer extends Human { // Have Developer inherit from Human
    constructor (firstName, lastName, type) {
        super(firstName, lastName); // call Human constructor but in this context
        this.type = type;
    }
    // Instances created from Developer constructor have a fullNameAndLanguage method
    fullNameAndLanguage(){
        return `${super.fullName()} develops ${this.type}`;
    }
    // Add static helper function to Developer
    static isJSdev (cody){
        return cody.language.toLowerCase() === 'javascript';
    }
}

// Create an instance of Developer
let cody = new Developer('Cody', 'Lindley', 'JavaScript');

// Call fullNameAndLanguage() method
console.log(cody.fullNameAndLanguage()); // logs "Cody Lindley develops JavaScript"

// Call fullName() method
console.log(cody.fullName()); // logs "Cody Lindley"

// Call static type of Developer
console.log(Developer.isJSdev(cody)); // logs true

The details of a class object will be broken down in this chapter. For now, you should observe from the newer class syntax the following:

  1. The use of the class, extend, constructor, super, and static keywords that were added to JavaScript to simplify prototypal inheritance for object-oriented minded developers (i.e. cleaner, concealing prototypal nuances, less boilerplate).
  2. The use of shorthand method names within the class object. Which is the only option!
  3. No commas separating the constructor function, method functions, or static method functions in the class object.
  4. Notice how prototype boilerplate is eliminated and referencing an inherited class is trivial using super.

6.2 : The class Expression v.s. class Declaration

A class can be defined using either a class declaration or class expression.

A class declaration:


class Human {
    constructor (firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    // class name can be used to reference the class inside the class object
    classMethod (){
        console.log(Human); // logs out class object backing property function
    }
}

const jill = new Human();

// call classMethod and log to the console reference to class, from inside of the class
jill.classMethod();
                

A class expression:


const Human = class optionalClassNameForReferenceInClassMethods {
    constructor (firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    // the optional name can be used to reference the class inside the class object
    classMethod (){
        // logs out class object backing property function
        console.log(optionalClassNameForReferenceInClassMethods);
    }
}

const jill = new Human();

// call classMethod and log to the console reference to class, from inside of the class
jill.classMethod();
                

Notes:

  1. Class declarations are not hoisted.
  2. Both class declarations and class expression bodies are executed in strict mode.
  3. A class declaration can be declared once and any other declarations will throw a type error. A class expression can be re-defined to another class expression but not a class declaration. Doing so will also throw a type error.
  4. A class can not be called without the new keyword.
  5. When using class syntax the prototype of the class (e.g. Human.prototype) is read-only.
  6. Class definitions are first class citizens (i.e. you can pass them to functions, return a class from a function, and assigned them to variables).

6.3 : Using a Class constructor Method

Each class definition can have exactly one constructor function that is invoked when an instance of the class is instantiated. Typically, the constructor function will define the initial properties for instances created from the class.


// create a Human class that expects a first and last name value when instantiated
class Human {
    constructor (firstName, lastName) {
        console.log('constructing');
        // the this keyword, used below, is the new object create when calling Human
        this.firstName = firstName; // create a firstName property and give it a value
        this.lastName = lastName; // create a lastName property and give it a value
    }
}

const may = new Human('May','Jones'); // logs 'constructing'

console.log(may);
/* logs:
[object Object] {
    firstName: "May",
    lastName: "Jones"
  }
*/
                        

Notes:

  1. A constructor function is optional. If you omit one, a blank constructor is created for you (i.e. constructor(){}).
  2. The keyword super(), within the constructor is used to call the parent class (i.e. the class, a class inherits or is extend'ed from).

6.4 : Using a Class Method

Class methods can be called on instances of the class. When called, the method of the class uses the values defined on the instance via the this keyword.


// create a Human class that is passed a first and last name value when instantiated
class Human {
    constructor (firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    // Instances created from Human class have a fullName class method
    fullName(){
        // this inside a class method refers to the instance the method is called on
        return `${this.firstName} ${this.lastName}`;
    }
}

const may = new Human('May','Jones'); // may is an instance of the Human class
const bill = new Human('Bill','Jones'); // bill is an instance of the Human class

console.log(may.fullName()); // logs "May Jones"
console.log(bill.fullName()); // logs "Bill Jones"
    

The fullName class method is defined once for all instances.

Notes:

  1. Class methods are non-enumerable by default.
  2. Class methods are written using the shorthand method naming.
  3. The keyword super.[class method] within a class method can reference and or call the methods of the parent class (i.e. the class, a class inherits or is extend'ed from).
  4. Getters and Setters (aka Accessors descriptors or computed properties) can be used on class methods (i.e. class MyClass { get MyMethod(){...} set MyMethod(x){...} }).
  5. Computed property names can be used within class definitions (i.e. ['method'+'Name'](){ ... }).

6.5 : Using a static Class Method

While class methods are called on instances of a class, static class methods are called from the class itself. Don't over think this setup. This is the different between something like Array.isArray() and [].forEach(). Array.isArray() is a static method on the Array "class" itself while [].forEach() is a "class" method called on instances of an Array


// Define a Human class with a static method
class Human {
    static isHuman(classInstance){
        // is the constructor of the classInstance the same as this class i.e. Human = Human,
        return classInstance.constructor === this;
    }
}

// create an instance of the Human class
var pat = new Human();

// call static method on Human class
Human.isHuman(pat); // logs true

    

Notes:

  1. Static methods are also inherited.
  2. From within a static method, you can refer to other static methods with this.staticMethodName. But inside a constructor or class method you will have to either use ClassName.staticMethodName or this.constructor.staticMethodName.

6.6 : Using extend to Inherit Methods from Another Class

A class can be sub-classed (i.e. Human > Developer) when using the extend keyword upon definition. For example, in the code below when defining the Developer class we use the extend keyword to link/inherit the parent Human class to the child Developer class.


// Create a Human class, i.e. an object factory
class Human {
    constructor (firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    // Instances created from Human constructor have a fullName method
    fullName(){
        return `${this.firstName} ${this.lastName}`;
    }
}

// Create a Developer class
class Developer extends Human {
    constructor (firstName, lastName, type) {
        super(firstName, lastName); // like calling Human.call(this, firstName, lastName);
        this.type = type;
    }
}

// Create an instance of Developer
let cody = new Developer('Cody', 'Lindley', 'JavaScript');

// Call fullName() method, inherited from Human Class
console.log(cody.fullName()); // logs "Cody Lindley"
    

Notes:

  1. Built-in objects can be extended or sub-classed not just user-defined classes.
  2. This inheritance link gives scope access to Human from Developer using the keyword super.
  3. If a sub-class is missing a constructor, the parent constructor is called. If a sub-class does have a constructor, it will need to call super() with the expected arguments to call the parent's constructor.

6.7 : Using super to Call the Inherited Constructor

The super keyword within a constructor method is used to invoke the parent class from the child class. This invocation setups the needed properties for inherited methods to run correctly.


// Create a Human class, that other class's will use as a parent class
class Human {
    constructor (firstName, lastName) {
        console.log('Super() called this constructor from child Class');
        this.firstName = firstName;
        this.lastName = lastName;
    }
    // Instances created from Human constructor have a fullName method
    fullName(){
        return `${this.firstName} ${this.lastName}`;
    }
}

// Create a Developer class, that is a child class of Human
class Developer extends Human {
    constructor (firstName, lastName, type) {
        // super has to be used first, if used
        super(firstName, lastName); // like calling Human.call(this, firstName, lastName);
        // the above, using a parent constructor, does this:
        // this.firstName = firstName;
        // this.lastName = lastName;

        this.type = type;
    }
}

// Create a Lawyer class, that is a child class of Human
class Lawyer extends Human {
    constructor (firstName, lastName, type) {
        // super has to be used first, if used
        super(firstName, lastName); // like calling Human.call(this, firstName, lastName);
        // the above, using a parent constructor, does this:
        // this.firstName = firstName;
        // this.lastName = lastName;

        this.type = type;
    }
}

// Create an instance of Developer, both the Developer and Human constructors are invoked.
const cody = new Developer('Cody', 'Lindley', 'JavaScript');
console.log(cody.fullName())
// works because Developer has a firstName and lastName setup by calling super()

// Create an instance of Lawyer, both the Lawyer and Human constructors are invoked.
const lisa = new Lawyer('Lisa', 'Lindley', 'Criminal');
console.log(lisa.fullName())
// works because Lawyer has a firstName and lastName setup by calling super()

    

Notes:

  1. The super keyword should be the first expression in a constructor function (i.e. before the this keyword is used).

6.8 : Using super to reference Inherited Methods

The super keyword when used within a class method or static method will be a reference to the parent class's methods.


class Human {
    constructor (firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    fullName(){
        return `${this.firstName} ${this.lastName}`;
    }
}

class Developer extends Human { // Have Developer inherit from Human
    constructor (firstName, lastName, type) {
        super(firstName, lastName);
        this.type = type;
    }
    fullNameAndLanguage(){
        // use super keyword to reference parent class method, and invoke it.
        return `${super.fullName()} develops ${this.type}`;

        // Note referencing super alone will throw an error i.e. console.log(super);
    }
}

// Create an instance of Developer
const cody = new Developer('Cody', 'Lindley', 'JavaScript');

// Call fullNameAndLanguage() method
console.log(cody.fullNameAndLanguage()); // logs "Cody Lindley develops JavaScript"
    

6.9 : Extending JavaScript Built-in's and Web API Constructors/Classes using class Syntax.

Before ES2015 class syntax sub-classing/extending a built in constructor/class like Array() had significant limitations.

Today, extending the JavaScript Array() class is trivial using class syntax.


class MyCustomArray extends Array{
    customEntriesMethod(){
        return Object.entries(this);
    }
}

let myCustomArrayInstance = new MyCustomArray('one', 'two', 'three');

myCustomArrayInstance.push('four');

console.log(myCustomArrayInstance.customEntriesMethod());
// logs [["0", "one"], ["1", "two"], ["2", "three"], ["3", "four"]]
    

Even, JavaScript web API's like the DOM can be extended.

Notes:

  1. If you run the above code through Babel is will break. Sub-classing constructors/classes has to be supported natively. Transpiling and polyfilling can't help and in fact, will break the code. The environment either supports extending built-in classes/object or it does not. If you extend natives make sure you are not also babel'ifying/transpiling the code.

Chapter 7 : ES2015 Promises

This chapter will examine the need for JavaScript Promises and then explain their usage.

7.1 : Asynchronous Programming Basics

Code that does not run completely as part of a normal execution cycle is said to run asynchronously. Asynchronously executed code in this context basically means that one defines code now to occur later in the future while not blocking other code from running synchronously. The simplest example of this is setTimeout().


// execute the passed function in 10'ish seconds in the future
setTimeout(() => console.log('Ten seconds ago, you ask me to run this code'), 10000);

// But keep executing code, don't wait for 10secs, and block all code execution
console.log('I am not delayed by 10 seconds from running');
        

The function passed to setTimeout() is known as an asynchronous callback function. This function will run 10'ish seconds in the future without blocking other code from running. Consider that the setTimeout() function argument is not all that different from a callback function for a click event or a callback function used to capture the response from an XMLHttpRequest network request.

Basically, when you ask JavaScript to wait to run some code, while not blocking the rest of the program from running, you are dealing with asynchronous code.

Notes:

  1. An in-depth understanding of asynchronous programming in JavaScript requires an understanding of the call stack and the event-loop. Here is a simple and concise review of these parts, "Help, I'm stuck in an event-loop.". If you've struggled in the past with understanding promises it is likly because you lack the foundational information found in the aforementioned video.

7.2 : Asynchronous Programming Before ES2015 (Or, Why Promises?)

Callback functions before ES2015 were the typical means in which one dealt with asynchronous programming situations. And even today, a simple callback function works just fine in many situations (e.g. the user clicks on a button and the corresponding callback function is run asynchronously).

However, asynchronous situations do exits that make using callback functions buggy and laborious. These situations can involve complex network exchanges found between a client and a server. Imagine you need to make five different requests to the server from the client and the requests have a complex relationship with each other. For example, imagine that the first request has to finish before the second and third request can begin. And both the second and third request have to finish before the fourth and fifth request can begin. And, the fourth and fifth request are not both required to be finished, you only need one to finish, whichever one is first. Also imagine, for each request you have to create an error handling situation.

I hope you can see that using callbacks alone, to complete the task just mentioned, using something like the XMLHttpRequest web API will either lead to a pyramid of callback functions:


// A function within a function within a function within a function etc...

getData1(function(x){
    ...
    getMoreData2(x, function(y){
        ...
        getMoreData3(y, function(z){
            // on and on it could go
        });

    });

})();

// oh no the pyramid of doom!

Or, a long list of linked callback functions:


// function 1 calls function 2, function 2 calls function 3 etc...

getData1(function(x){
    ...
    getMoreData2(x)
})();

getData2(function(y){
    ...
    getMoreData3(y)
});

getData3(function(z){
    // on and on it could go
});

Either way, many consider this situation, callback hell.

To combated the complexity of complicated asynchronous programming and avoid callback hell ES2015 provided the native Promise API as an alternative to callback functions alone. The remainder of this chapter will cover the new Promise API in further detail.

Notes:

  1. Originally, promises were called "Futures" and were a part of the DOM spec. Thankfully, in the end it ended up in the ECMAScript specification so all JavaScript runtimes could use Promises for asynchronous programming.
  2. Unfortunately, IE does not support promises and it will have to be polyfilled if you need IE support. However the Edge browser (12+) does have support.

7.3 : Producing & Consuming a Promise

An instance of a Promise is basically a function that typically houses asynchronous routines with two special functions to be called once the asynchronous work either completes or fails (i.e. resolve() or reject()). In short, a Promise provides the boilerplate around asynchronous code which results in an interface/methods for working with asynchronous code. Consider the methods and static methods provided by Promises:

  • Promise.all()
  • Promise.prototype.then()
  • Promise.prototype.catch()
  • Promise.prototype.finally() (ES2018)
  • Promise.race()
  • Promise.reject()
  • Promise.resolve()

These methods and static methods provide a cleaner API when dealing with complicated relationships among asynchronous code.

To produce a promise one only need to construct an instance of a promise from the Promise constructor passing the constructor one argument known as the executor function. The executor function is passed two argument functions, the first is called resolve, and the second argument is called reject.


// Create a Promise called myPromise
const myPromise = new Promise(
    (resolve, reject) => { // this is an executor function
        try{
            // do some async work, like an XHR request or a setTimeout()
            // ...
            // then call resolve() when done, pass it some data
            setTimeout(function(){resolve('foo');}, 1000);
            // comment out the line above and un-comment line below to see error thrown
            // foo;
        }catch(error){
            // if an error occurred, call reject()
            reject(error);
        }
    }
);

// Consume myPromise
myPromise.then( // takes two functions,
    // if promise calls resolve() this first function is called
    (data) => { console.log(data); }, // logs 'foo'
    // if promise calls reject() this second function is called
    (error) => { console.log(error.toString()) } // logs Error
);

Let's take the promise API for a spin. In the code example below I am wrapping a Promise around the older XMLHttpRequest object and concealing its older callback function API in order to create a mini Github Promise based API.


// Create a function that will return a Promise

const getGitHubData = (gitHubRESTPath) => {
    // return a Promise so the .then() method can be called on returned value
    return new Promise((resolve, reject) => { // this is the executer function
        // start asynchronous work
        const xhr = new XMLHttpRequest();
        xhr.onload = function () { // callback function
            if (this.status === 200) {
                // call resolve() with async results when async work is done
                resolve(this.response);
            } else {
                // call reject() with statusText if server returns anything but a 200
                reject(this.statusText);
            }
        };
        xhr.onerror = function () { // callback function
            // call reject() with error message if XHR errors occur
            reject('XHR Error:');
        };
        xhr.open('GET', 'https://api.github.com/' + gitHubRESTPath);
        xhr.send();
    });

};

// Now call getGitHubData() and consumer the Promise wrapped around XMLHttpRequest

// Get number of stars for React on github
getGitHubData('repos/facebook/react').then( // use then() to consume the eventual results
    //resolve function
    (response) => {
        // convert the JSON string response to a JS object
        console.log(JSON.parse(response).stargazers_count); // logs 11XXXX
    },
    //reject function
    (error) => {
        console.log(error);
    }
);

// Send a bad URL, to see reject function run
getGitHubData('repos/nothing/nothing').then( // use then() to consume the eventual results
    //resolve function
    (response) => {
        // convert the JSON string response to a JS object
        console.log(JSON.parse(response).stargazers_count);
    },
    //reject function
    (error) => {
        console.log(error); // logs 'Not Found'
    }
);

By wrapping the Promise itself with the getGitHubData function I can call the getGitHubData function like an API. The "mini api" provides a cleaner interface for working with asynchronous HTTP requests to the Github REST API because a Promise is used/returned.

To consume the returned promise the then() method is used to capture both the resolution of the asynchronous code and potentially any resulting errors (i.e. then() takes two arguments. First argument is resolution function, called when promise is resolved, second is a rejection function, called if the promise is rejected).

Notes:

  1. The executor function pass to Promise() is called before the Promise constructor returns the created object.
  2. If an error is thrown in the executor function the promise is rejected.
  3. Promises have three states or life cycles (pending > resolved || rejected > settled). The promise starts in a pending state. Then it either moves to a fulfilled (i.e. resolved) or rejected state. Once in a fulfilled or rejected state the promise is considered to be settled. Once settled, a promise can't change its state. Settled means, settled.
  4. One should consider a Promise as a stand-in for a value that has yet to be determined. In short, you use a promise to wrap asynchronous routines and then use promise methods to provide callback functions for the routines.

7.4 : Consuming Promises

Since promises are native to the language several JavaScript runtimes can make use of Promises. For example, the new Fetch API, which replaces the older XMLHttpRequest API returns a promise by default. Thus, instead of having to create a promise, all you have to do is consume the promise returned from calling fetch().

I've re-written the code from the previous section to use the new Fetch API. By using the Fetch API I don't need to produce a Promise object manually, one is simply given to me by the web platforms Fetch API. As you can see in the code example below when a promise is returned all that is left to do is to consume the promise using promise methods (e.g. then() method)


// Get number of stars for React on github
fetch('https://api.github.com/repos/facebook/react')
    .then( // use then() to consume the eventual results
        //resolve function
        (response) => {
            // note that calling response.json() is itself a promise
            response.json().then(json => console.log(json.stargazers_count)); // logs 11XXXX
        },
        //reject function
        (error) => {
            console.log(error.toString()); // logs "undefined"
        }
    );

// Send a bad URL, to see reject function run
fetch('htttps://www.badnogoodurl.com')
    .then( // use then() to consume the eventual results
        //resolve function
        (response) => {
            // note that calling response.json() is itself a promise
            response.json().then(json => console.log(json.stargazers_count)); // logs 11XXXX
        },
        //reject function
        (error) => {
            console.log(error.toString()); // log "undefined"
        }
    );

7.5 : Producing an already Resolved or Rejected Promise

The Promise API offers the Promise.resolve() and Promise.reject() static methods that will produce an instance of a Promise that has either been resolved or rejected with a given value. This can be handy when needing to quickly create a promise in a specific state without dealing with an executor function.

The Promise.resolve() static method returns a resolved promise with a specific value passed to it:


Promise.resolve('value1')
    .then(value => console.log(value)); // logs "value1"

// The above is a shortcut for this:
new Promise(resolve => resolve('value2'))
    .then(value => console.log(value)); // logs "value2"

The Promise.reject() static method returns a rejected promise with a specific value passed to it:


Promise.reject('error1')
    .then(() => {}, error => console.log(error)); // logs "error1"

// The above is a shortcut for this:
new Promise((resolve, reject) => {reject('error2');})
    .then(() => {}, error => console.log(error)); //logs "error2"

Don't over think these two static methods they simply side step the executor function and return a promise in a specific state.

Notes:

  1. The Promise.resolve() static method is typically used to wrap a Promise around a value.

7.6 : Chaining Promises with then()

The then() method is used to provide a resolution and rejection function for a promise. Remember, a promise runs/contains asynchronous code. This asynchronous code when complete can call one of two functions either resolve() or reject(). The then() method accounts for both of these calls. If resolve() is called then the first function to then() is invoked. If reject() is called then the second function pass to then() is called.

In the code example below I am using then() to extract the commits on Github for React, then extract the author of the last commit, then log that author to the console.


// get the name of the last person to commit to the React github repository

// fetch all commits for React
fetch('https://api.github.com/repos/facebook/react/commits')
    // handle response, pass json to next then()
    .then(response => response.json(), error => console.log(error))
    // fetch user from Github API
    .then((json) => {
        // use commit data to get user name of last person to commit
        // use user name to get author data
        // pass response to next then()
        return fetch('https://api.github.com/users/'+json[0].author.login);
    }, error => console.log(error))
    // handle response pass json to next then()
    .then(response => response.json(), error => console.log(error))
    // get name, pass it to the next then(), note I am not passing a promise but just a simple value
    .then(user => user.name, error => console.log(error))
    // log the name of the last person to commit code to the react repository
    .then(name => console.log(name + ' is the last person to commit to React repository'));

It is important to remember, and you may have noticed in the previous code example, that the then() method can be used to chain any value not just promises. In the code example below I demonstrate using the promise API to simply transfer data through a promise chain.


// create a promise that is already resolved with a specific 'foo' value.
var myPromise = Promise.resolve('foo');

// show how a promise can chain any value indefinitely.
myPromise.then((data)=>{
    return data;
}).then((data)=>{
    return data;
}).then((data)=>{
    console.log(data); // logs 'foo'
});

However, the then() method was specifically designed to chain promises that are doing asynchronous activities. In the following code example I use the promise chain to verify all the starwars API endpoints are functioning.



let starWarsAPICheck = [];

console.log('Wait for it...! Data from another Galaxy yo!');

// chain a set of promise to occur one after the other
// check the starwars api to see if all endpoints are working.
fetch('https://swapi.co/api/people')
.then((response)=>{
    starWarsAPICheck.push(response.ok);
    return fetch('https://swapi.co/api/planets');
}).then((response)=>{
    starWarsAPICheck.push(response.ok);
    return fetch('https://swapi.co/api/films');
}).then((response)=>{
    starWarsAPICheck.push(response.ok);
    return fetch('https://swapi.co/api/species');
}).then((response)=>{
    starWarsAPICheck.push(response.ok);
    return fetch('https://swapi.co/api/vehicles');
}).then((response)=>{
    starWarsAPICheck.push(response.ok);
    return fetch('https://swapi.co/api/starships');
}).then(()=>{
    console.log(starWarsAPICheck); // logs [true, true, true, true, true, true]
});

// Note the previous chain all happen sequentially
// To make all the calls run in parallel, Promise.all() could be used.

One should note that any error returned within the first function passed to then() will result in the next then() running the rejection function (i.e. the second function padded to then()).


// create a promise that is already resolved with a specific a 'foo' value.
var myPromise = Promise.resolve('foo');

// show how error returned in the second then(), calls reject callback function, but chain goes on
myPromise.then((data)=>{
    return data;
}).then((data)=>{
    throw Error('yo this then() is no good');  // will cause reject callback in next then() to run
}).then(
    (data)=>{console.log(data)}, // does not run
    (error)=>{console.log(error.toString())}, //logs 'yo this then() is no good'
).then((data)=>{
    console.log('note this still runs');
    console.log(data); // logs undefined, foo is undefined
});

Basically, using then() allows a developer to push data through a set of functions that can deal with both synchronous and asynchronous results interchangeable. When an error occurs, the chain does not stop, the next then() simply captures the error and runs the second function passed to it.

Notes:

  1. If you don't provide then() a resolve function or provide a non-function, no error will occur. The next then() resolve function is passed an undefined value.

7.7 : Catching Rejections in the Chain with catch()

When rejection errors occur within a promise chain the catch() method can be used to capture the rejection. This can be handy for two reasons. First off, you can write a single error handle for a long list of chained promises and the first rejection in the chain will get passed on to the catch(). Secondly, because catch() returns a promise, just like then(), you can keep chaining with more then()'s or catch()'s. Basically, one could perform several asynchronous routines and use one catch() if any errors occur, then continue on with more then()'s or catch()'s' at will.

In the code example below a broken API call is kicked off and a catch() method at the end of the chain captures the error.



fetch('https://api.github.com/repos/fanebook/react/commits') // broken URL, facebook not fanebook
    .then(response => response.json())
    .then((json) => {
        // this throws an error because the json passed in is bad
        // it is bad because the original URL is wrong facebook, not fanebook
        // so json[0].author.login is a bad reference
        // so this function throws an error which is caught in the first catch() in the chain
        return fetch('https://api.github.com/users/'+json[0].author.login);
    })
    .then(response => response.json()) // response.json() returns a promise
    .then(data => console.log(data.name + ' is the last person to commit to React repository')) // never logs
    .catch(error => { // any error issue in the previous then()'s are trapped here
        console.log(error.toString());
    })
    .then(() => {
        console.log('yup you can still chain after a catch()');
    });

Notes:

  1. The catch() method is typically used over providing each then() with a rejection parameter.
  2. Notice how readable the code in this section becomes once a single rejection function was used via a catch() over providing a rejection function for each then().
  3. The catch() method is simply a short-hand for, .then(null, error => { // do something with error }).

7.8 : Running a Final Function, Regardless of Promise Fulfillment State with finally()

When a situation exists that you'd like to run a function regardless of if a promise is resolved or rejected you can use the finally() method. This method makes it possible to run code no matter what occurs previously in the promise chain.

In the code example below I demonstrate how the finally() method is invoked regardless of the fulfillment paths through the promise chain.


let stateOfLoading = 'Not Loading'; // this is the default state

fetch('https://swapi.co/api/peoples/') // breaks because the URL is people not peoples
    .then((response) => {
        if(!response.ok){ // run if fetch fails
            throw Error('api call broke'); // catch() is called
        }
        stateOfLoading = 'Loaded';
        return stateOfLoading; // passed from finally() to the last then() in chain
    })
    .catch(error => {
        stateOfLoading = 'Loading Error';
        return stateOfLoading; // passed from finally() to the last then() in chain
    })
    .finally(() => { // finally does not take in parameters
        // runs no matter what, because we want to return state to default
        // regardless of if the catch or then is fulfilled
        stateOfLoading = 'Not Loading';
    })
    /* Basically a finally() eliminates redundant functions in a then()

    The above finally is just shorthand for using a then() like so:
    .then(
        () => { // finally does not take in parameters
        // runs no matter what
        stateOfLoading = 'Not Loading';
        },
        () => { // finally does not take in parameters
        // runs no matter what
        stateOfLoading = 'Not Loading';
        },
    )
    */
    .then((data) => { // get value from previous catch() or then()
        // change the initial api call from /people to /peoples to get different values
        console.log(data); // either 'loaded' or 'loading error'
        console.log(stateOfLoading); // verify finally statement returned state to default
    });

Notes:

  1. The finally() method was added in ES2018.
  2. This method is typically used to clean up things that might have been changed while a chain of Promises are being resolved (i.e. turn a loader UI off and return either data or an error in place of the loading UI).

7.9 : Waiting for a List of Parallel Promises to Complete with Promise.all()

If you want to kick off several asynchronous activities simultaneously using promises and get notified when they are all complete the Promise API offers Promise.all(). The Promise.all() static method takes an array of promises and will return a single promise when all the promises in the Array are fulfilled.



Promise.all([
    fetch('https://api.github.com/repos/facebook/react/commits'),
    fetch('https://api.github.com/repos/facebook/react/stargazers')
])
.then(([commits, stargazers]) => {
    console.log(commits.ok, stargazers.ok); // logs true, true
});

7.10 : Waiting for the First, of a List, of Parallel Promises to Complete with Promise.race()

If you want to kick off several asynchronous activities simultaneously using promises and get notified when the first promise fulfills the Promise API offers Promise.race(). The Promise.race() static method takes an Array of promises and will return only the first promise that is fulfilled.



Promise.race([
    fetch('https://api.github.com/repos/facebook/react/commits'),
    fetch('https://api.github.com/repos/facebook/react/stargazers'),
])
.then(response => {
    console.log(response.url); // logs whichever one finishes first
});

Chapter 8 : ES2017 Async Functions & await operator

This chapter covers asynchronous functions (i.e (async () => {})();) and the use of the await operator with async functions so that asynchronous code can be written to look more like synchronous/procedural code.

8.1 : The async/await syntax is not a replacement for promises

It would be a mistake thinking async functions and the await operator completely replaces the use of promises. Or, that one need not clearly understand promises before using async functions and the await operator. Thus, an in-depth reading of the previous chapter is necessary to comprehend this chapter.

An async function returns a promise that is typically resolved using the await operator. Think of an async function as a syntactical replacement for an executor function and the await operator as an inline syntactical replacement for then().

Ultimately what you need to keep in mind is that the async/await syntax enhances promises, it does not replace promises!

8.2 : The benefits of using async functions and the await operator

Using async functions and the await operator to handle asynchronous code is a stylistic choice with some subjective benefits over traditional promises.

In the code example below I've augmented the Github promise code from the previous chapter creating a utility function that uses an async function and the await operator instead of a long .then() promise chain.


let stateOfLoading = 'Not Loading'; // this is the default state

// add async keyword to arrow function, now this function returns a promise and can use await operator
const getLastGitHubCommitAuthor = async (owner, repo) => {
    try {
        stateOfLoading = "Loading";
        // await pauses this function until the promise from fetch is fulfilled
        const commits = await fetch(`https://api.github.com/repos/${owner}/${repo}/commits`);
        // await pauses this function until the promise from .json() is fulfilled
        const jsonCommits = await commits.json(); // json() returns a promise
        // await pauses this function until the promise from fetch is fulfilled
        const author = await fetch(`https://api.github.com/users/${jsonCommits[0].author.login}`);
        // await pauses this function until the promise from .json() is fulfilled
        const jsonAuthor = await author.json(); // json() returns a promise
        return jsonAuthor.name; // a string is returned, but it is wrapped in a Promise
    } catch (error) { // any and all errors from the try and caught here, sort of like .catch()
        stateOfLoading = 'Error Loading';
        throw new Error(error); // throw error so async function will return rejected promise.
    } finally { // can replace the .finally() method functional from a promise chain
        stateOfLoading = 'Not Loading';
    }
};

// Now consume the async function, which returns a promise, with promise methods.
getLastGitHubCommitAuthor('facebook', 'react')
    .then(name => console.log(name + ' is the last person to commit to the React repository'))
    .catch(error => console.log(error.toString()));

Now compare the async/await solution you just looked at to the promise only solution:


let stateOfLoading = 'Not Loading'; // this is the default state

const getLastGitHubCommitAuthor = (owner, repo) => {
    // explicitly return a promise from the end of the chain
    return fetch(`https://api.github.com/repos/${owner}/${repo}/commits`)
        .then(response => response.json()) // json() returns a promise
        .then(jsonCommits => fetch(`https://api.github.com/users/${jsonCommits[0].author.login}`))
        .then(response => response.json()) // json() returns a promise
        .then(jsonAuthor => jsonAuthor.name )
        .catch((error) => {
            stateOfLoading = 'Error Loading';
            throw new Error(error);
        })
        .finally(() => {
            stateOfLoading = 'Not Loading';
        });
};

// Now consume the async function, which returns a promise, with promise methods.
getLastGitHubCommitAuthor('facebook', 'react')
    .then(name => console.log(name + ' is the last person to commit to the React repository'))
    .catch(error => console.log(error.toString()));

You should note the async/await solution offers the following subjective benefits:

  • Notice how asynchronous code has moved closer to a synchronous/procedural style (i.e. do this, then do this, then do this). Many people find this easier to read and maintain than using method chaining boilerplate.
  • Notice the reduction of chaining boilerplate (i.e. only used one then() not a long chain of them).
  • Notice the efficiency of using a try, catch, and finally to capture both asynchronous and synchronous errors and run code regardless of where the error occurs. (i.e. similar to how .catch() and .finally() works for long promise chains). In general, avoiding callback functions passed to then() means less cruft around errors being thrown.
  • Consider also that using the async/wait syntax could flat out potential child chains (i.e. then() chains inside of then() chains) and simplify conditional asynchronous activity.

The benefits just mentioned are often touted as the benefits for using async functions and the await operator over promise chaining. In short, many simply consider the async/await syntax an intuitive and cleaner replacement for promise chaining. Personally, I simply consider the use of async/await as a way to avoid chaining hell which is simply a hell wrapped around callback hell.

Notes:

  1. The async keyword works with all functions; function expressions (e.g. const foo = async function () {};), function declarations (e.g. async function foo() {}), method definition (e.g. let obj = { async foo() {} }), and the arrow function (e.g. const foo = async () => {}).
  2. Don't forget, the await keyword only works inside a async function and is used to pause a function until a promise is fulfilled.
  3. The async keyword placed in front of a function creates a special kind of function object called, "AsyncFunction". The AsyncFunction constructor is not a global object but you can obtain a reference to the constructor using Object.getPrototypeOf(async function(){}).constructor.

8.3 : Asynchronous functions return an implicit Promise

If a promise is not explicitly returned from an async function then one is implicitly returned. The result is that a promise is always returned from an async function.


// an async function that returns the value 5
var myAsyncFunction = async () => {
    return 5;
}

// verify async returns an implicit promise, wrapping the value 5
console.log(myAsyncFunction() instanceof Promise); // logs true

// use then() to get value returned from myAsyncFunction
myAsyncFunction().then((value) => { console.log(value) }); // logs 5

8.4 : The await operator awaits a Promise or creates an implicit Promise

Any value await'ed for that is not a Promise is implicitly converted to a resolved promise. (e.g. Promise.resolve(value)).


// an async function that returns the value 5
var myAsyncFunction = async () => {
    const awaitedPromiseValue = await 5;
    // The above is like explicitly writing:
    // const awaitedPromiseValue = await Promise.resolve(5);
}

// use then() to get value returned from myAsyncFunction
myAsyncFunction().then((value) => { console.log(value) }); // logs undefined
// nothing was returned so implicitly undefined is returned
// i.e. Promise.resolve(undefined);

Notes:

  1. If a promise that is being await'ed for is rejected the await expression throws the rejected value.

8.5 : The await operator is sequential

It might be obvious but just like the then() method the await operator waits for promise resolution/fulfillment before moving on to the next line of code so to speak. This means to run parallel asynchronous routines instead of sequential routines the static Promise methods Promise.all() and Promise.race() can be used.


(async () => { // create async function that is immediately invoked

    // sequential fetch's
    const responsePlants = await fetch('https://swapi.co/api/planets');
    // API call above has to finish before the API below can start
    const responseFilms = await fetch('https://swapi.co/api/films');

    console.log(responsePlants.ok, responseFilms.ok) // logs true true

    // parallel fetch's, faster than sequential fetch's using Promise.all()
    const [parallelResponsePlanets, parallelResponseFilms] = await Promise.all([
        fetch('https://swapi.co/api/planets'),
        fetch('https://swapi.co/api/films')
    ]);

    console.log(parallelResponsePlanets.ok, parallelResponseFilms.ok) // logs true true

    // parallel fetch's, return first one to finish, using Promise.race()
    const fastestResponseFromStarWarsApi = await Promise.race([
        fetch('https://swapi.co/api/species'),
        fetch('https://swapi.co/api/vehicles'),
        fetch('https://swapi.co/api/starships')
    ]);

    console.log(fastestResponseFromStarWarsApi.ok); // logs true

})()

Chapter 9 : Using ES2015 Modules Today

This chapter will focus on defining the need for modules in general, the difference between module syntax and the module loading API, and then briefly explore the browsers module loading API.

9.1 : Why JavaScript Modules (aka EcmaScript Modules)?

Before ES2015 JavaScript didn't offer native modules. Oh, everyone faked it for years in all sorts of amazing ways but all of these solutions were temporary fixes to a significant deficit with the language. What was needed was a native module syntax built into the language and a runtime loading system that offered the following:

  • Modules with a native private scope that did not have to reach into the global scope to talk to other modules (i.e. the ability to import and export modules from within modules without reaching into the global scope).
  • Explicitly defined dependencies within a module (i.e. import).
  • Explicitly defined values within a module that can be shared with other modules (i.e. export).
  • A runtime loading API with a native runtime dependency tree.

All of the above requirements as of today have been satisfied by:

  • The JavaScript module syntax (e.g. import and export)
  • Runtime module loading API's. As an example, the web platforms Javascript module loading API that loads module files (i.e. <script type="module" src="myModule.js" />)

Notes:

  1. JavaScript modules are based on CommonJS modules (aka Node.js modules). In the future, JavaScript modules will likely replace commonJS modules. In fact, JavaScript module syntax was introduced experimentally into Node.js v8.5.0. But keep in mind imported values in CommonJS are copies of values while imported values in ES modules are live read-only values.

9.2 : Understanding Module Syntax V.S. the Module Loader API

JavaScript modules require two separate parts/specifications working together. The first part specifies how the module is defined and the implications of this definition within the language. (e.g. import and export). The second part is how the module is loaded (e.g. The browser module loader API e.g. <script type="module" src="myModule.js" /> and the Node ES module Loading API).

The module syntax is what is defined by the ECMAScript standard. The loader API is not defined by the ECMAScript standard. The details on how a module is loaded are details that JavaScript runtimes have to iron out (e.g. Browsers and Node.js).

Notes:

  1. Still, as of 2019, it is common to see module bundlers (e.g. Webpack and Parcel) used in place of a native module loading API (e.g. developers use bundlers today to transform ES module syntax into historical ES5 script files and these files don't use the native web module loading API).

9.3 : Loading/Running Static Modules in a Browser using <script type="module">

Before examining how static modules are loaded by browsers lets recap how the <script> element was used historically to load/run non-ES2015 JavaScript modules (aka script files, classic script files, or historical script files instead of ES modules).

Below three external <script src=""></script>'s' and one inline <script></script> are run in the the HTML document:


<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
</head>

<body>

    <script src="script1.js"></script>
    <script src="script2.js"></script>
    <script src="script3.js"></script>
    <script>console.log(this) // logs window object</script>

</body>
</html>
    

Note the following about the above loading/running of the script1.js, script2.js, and script3.js files:

  1. Values define in script files loaded into an HTML document are defined in the global scope (except for function and block scoped values contained in the script files).
  2. By default, the script1.js is loaded/parsed by the browser engine before moving on to script2.js (unless the async attribute is used). By default, scripts are loaded/parsed synchronously.
  3. By default all <script>'s' block the HTML parser (unless the defer attribute is used).

The new browser module loading API offers a new system for loading JavaScript modules that use ES module syntax. The HTML document below is evaluating modules instead of historical script files (Consider module1.js, module2.js, module3.js to be files in the same directory as the HTML file).


<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
</head>

<body>

    <!--  Entry module is module1.js, dependency graph starts here -->
    <script type="module" src="./module1.js"></script>
    <!--  module1.js imports and uses module2.js -->

    <!-- module1.js
        import text from "./module2.js";
        console.log(text); // logs "Hello from module 2";
    -->

    <!-- module2.js
        const text = "Hello from module 2";
        export default text;
    -->

    <!--  Entry module is inline script, dependency graph starts here -->
    <!--  inline script below imports and uses module3.js -->
    <script type="module">
        import text from './module3.js';
        console.log(text) // logs "Hello from module 3";
    </script>

    <!-- module3.js
        const text = "Hello from module 3";
        export default text;
    -->

</body>

</html>
    

The implications of loading JS modules using the browser module loading API are as follows:

  1. The type="module" attribute is required so the browser will use the native browser module loading API to parse/load each module.
  2. Each <script> has its own entry file and dependency graph.
  3. An ES module can be written inline or be written in a separate file and pulled into the current HTML file using the src attribute.
  4. An ES module runs code in strict mode by default.
  5. Values defined in an ES module that are not export'ed are scoped only to that module, these values don't leak into the global scope.
  6. ES modules are singletons in the sense that only a single instance of it exists. Basically, only one instance of it is used no matter how many times it is import'ed.
  7. ES Modules still have access to the global context (i.e. the top level runtime scope). In a browser this would be the global window context. However, the use of the keyword this will not work inside of a module to reference the global runtime scope.
  8. The use of the import and export syntax can be used within the module to import other modules and export values, so other modules can import those values. By doing this you create a graph of dependencies between modules that the loading API uses to figures out how to run the JavaScript (i.e. managed dependencies).
  9. Exports from an ES module are static. Meaning, once they have been defined and imported they can not be changed later.
  10. ES Modules and their dependencies (i.e. imported modules) are fetched with CORS.
  11. ES Module <script type="module">'s' use defer by default.

Notes:

  1. The following web browsers all support loading modules using the <script type="module">: Edge 17+, Firefox 61+, Chrome 63+, Safair 11.1+, iOS Safari 11.2+, Chrome Android 69+.
  2. Paths to modules can be full URLs, or relative URLs starting with /, ./, or ../. As of today, what won't work unless using an asset bundler, is specifying a module by name of module package (i.e. import {React} from 'react'). In other words, if you NPM install React you will have to import that module using a relative URL not the name of the package installed in node_modules.
  3. The file extension .js can be omitted from the URL specifier string.
  4. ES modules are static meaning they can't be changed at runtime. This allows things like static checking and optimizations when importing and bundling.
  5. You can request a module hosted on another domain if it uses CORS (e.g. import value from 'https://otherdomain.com/modules/module1.js';).
  6. Using the native web browser module loading API for systems that have more than 100+ modules could come at a performance cost (depends upon the size of dependency graph) and thus asset bundlers are in common use today to avoid performance related issues in production.
  7. All static module dependencies have to be downloaded and executed before the code will run. If you are wondering how you conditionally import a module you will have to use a dynamic import (i.e. import('module1.js')).
  8. Inside a module the statement import.meta will return an object containing a url property housing the location the module was imported from (e.g. console.log(import.meta); //logs { url: "file:///home/user/module1.js" }).

9.4 : Conditionally loading a Module using import()

The import statement has a dual purposed. It can be used to import other modules statically as well as dynamically. When the import statement is used as a function, and the module path/specifier is passed to it as an argument, a module can be loaded dynamically (e.g. import('./module1.js')) ). The return value from using import as a function is a Promise. Essentially this means you can asynchronously load ES modules after all the static modules have been loaded into the runtime.

In the code below after clicking anywhere on the HTML document, in the browser window, the lodash forEach ES module is loaded from the unpkg.com CDN and used to log out the context of an array.


<html>
<head>
    <meta charset="UTF-8" />
    <style>
        body {
            cursor:pointer;
        }
        p {
            position: relative;
            height: 100%;
            text-align: center
            }
        strong {
            position: absolute; top: 50%; 
            left: 50%; 
            transform: translate(-50%, -50%);
            font-weight: normal;
        }
    </style>
</head>

<body>
    <p>
        <strong>
        Click anywhere in this window <br>
        to load a module dynamically.<br> 
        Then view console.
        </strong>
    </p>
    <script type="module">
        const getModule = () => {
            (async () => { 
                // I am in an inline module, using import('module')
                const module = await import('https://unpkg.com/lodash-es@4.17.11/forEach.js'); 
                const forEach = module.default;
                forEach(['one','two'], console.log); 
            })()
        }
        // still have access to window from module (no this however)
        // run getModule when the body is clicked
        window.document.body.onclick = getModule;
    </script>
</body>
</html>
    

Notes:

  1. The following web browsers all support dynamic imports: Chrome 63+, Safari 11.1+, iOS Safari 11.2+, Chrome Android 69+.
  2. Paths to dynamic modules can be full URLs, or relative URLs starting with /, ./, or ../.

9.5 : Parsing JS Module Syntax with Asset Bundlers

Asset bundlers like Webpack and Parcel are in wide use today. These asset bundlers are used because they provide a system that will modularize and load many different asset types and formats. Not just JavaScript Modules! For example, Webpack and Parcel can treat things like JavaScript, HTML, CSS, and Image files like modules, that can be imported into each other, and then bundle these differing assets/formats into a production state.

Bundlers like Webpack and Parcel can also analyze JavaScript files written using ES module syntax and create from these files a dependency graph that is then used to output one or many (aka code splitting) production runnable ES5 files. The outputted JavaScript won't typically use the web browser module loading API or module syntax but instead will load as an ES5 script file would have before ES modules.

Basically asset bundlers when dealing with ES modules can read ES2015 module syntax and convert it to runnable bundled ES5 code in the form of a historical script file. Then these files are loaded into a browser using the historical <script> element.

Today, most developers use an asset bundler. The reason most developers choose a bundler over the browser loading API is a combination of performance concerns, browser support concerns, and the fact that bundlers will bundle not only JavaScript files, but also other collaborating assets (i.e. treating HTML, CSS, and image files like importable modules).

Notes:

  1. ES module syntax and semantics are lost when bundled by modern day assert bundlers.
  2. Asset bundlers can do much more than simply convert ES6 module syntax into runnable ES5 code. Fundamental to modern day bundlers is the process of taking ES modules syntax and JavaScript 2015+ code (via Babel) and converting it to script files that can be run in ES5 environments.
  3. Asset bundlers don't yet leverage the JavaScript module API found in modern web browsers (i.e. <script type="module" />).
  4. Using a package name to load a module won't work when using the browser module loading API. However, when using an asset bundler it is possible to use a packages name because the bundler can be configured to correctly find the path to the module identified by a name (e.g. import _ from 'lodash';).
  5. Rollup.js is a bundler just for JavaScript modules.
  6. Asset bundlers will typically accept CommonJS syntax or ES module syntax.

Chapter 10 : Writing ES2015 Module Syntax

This chapter will focus on ES modules syntax itself. ES Module syntax is commonly used by asset bundlers or more recently by the web browsers <script type="module"> module loading API.

10.1 : Module Syntax Overview

A lot of details and stylistic choices exist around the importing and exporting of values in JavaScript modules. However, fundamentally JS module syntax basically boils down to these two concepts:

  1. A module can export multiple values and or a single default value from within a module using the keywords export and default:

  2. 
    // myModule.js
    
    const value1 = 'value1';
    const value2 = 'value2';
    
    // export specific named values using export keyword
    export {value1, value2}; // named exports
    // Note export syntax, looks like destructuring syntax but is not destructuring
    
    // inline export
    export const value3 = 'value3';
    
    // default export, using default keyword
    export default value4 = 'value4';
        

  3. A module can simultaneously import multiple values and a single default value from other modules using the keywords import and from along with a URL specifier string identifier (i.e. the relative or full URL file path to the module whose values are being imported):

  4. 
    // some other module that imports values from myModule.js
    
    // import the default value and three named values from myModule.js
    import nameGivenToDefaultValue, {value1, value2, value3} from './myModule.js';
    // Note how the default value along with named values are imported together
    // Note export syntax, looks like destructuring syntax but is not destructuring
    
    // This module now has access to the imported values above
    console.log(nameGivenToDefaultValue, value1, value2, value3);
    //logs value4, value1, value2, value3
        

The rest of this chapter will break down some additional details about modules and the differing export'ing and import'ing styles that can be used when authoring JavaScript using ES module syntax.

Notes:

  1. An URL specifier must be a path to a module using full URLs, or relative URLs starting with /, ./, or ../. However, if one is using an asset bundler (e.g. webpack) the URL specifier can also be the name of the module package (i.e. import {React} from 'react') if the asset bundler has been configured to sort out the path to the module by package name.
  2. The file extension .js can be omitted from the URL specifier string.
  3. Imports are hoisted to the top of the scope regardless of where they appear in the module. Thus, most people place all imports at the start of a module.
  4. Where exporting happens (i.e. inline or end of the module) is subjective preference based on exporting styles.
  5. Imports and exports can't natively be conditionally imported or exported without the use of a dynamic import() (i.e. don't think you can place imports in conditional statements without using a dynamic import()).
  6. ES modules are in strict mode by default.
  7. ES modules have access to the global scope (e.g. window) but not using this value. The global scope has to be reference with a point to the global scope (e.g. window).
  8. Circular dependencies are supported (e.g. module 1 can import a value from modules 2, and at the same time module 2 can import value from module 1.). These modules depended upon each other and because modules are static this circular dependency is resolved before either module is executed.

10.2 : Importing and Exporting Named values

The export keyword can be used to export an unlimited number of named values from a module.

Three styles are available when exporting named values from a module:

1. Exporting values inline when expressed/declared:


// module1.js

// exporting values inline, when defining
export const simpleString = 'simpleString';
export const simpleFunction = () => {};
export const simpleNumber = 5;
export class myClass {};
export function myFunction(){};

// Note these won't work
// this will throw an error an expression or declaration is expected
export 5;
export simpleString;

2. Exporting values by reference:


// module1.js

const simpleString = 'simpleString';
const simpleFunction = () => {};
const simpleNumber = 5;
class myClass {};
function myFunction(){};

// style for multiple references
export {simpleString, simpleFunction, simpleNumber, myClass, myFunction};

3. Exporting renamed referenced values:


// module1.js

const sF = 'simpleString';
const sS = () => {};
const sN = 5;
class mC {};
function mF(){};

// exporting multiple renamed references
export {sF as simpleFunction, sS as simpleString, sN as simpleNumber, mC as myClass, mF as myFunction}

Four styles are available when importing the above named exported values into another module:

1. Importing a single named export:


// This is module2.js, import a single value into this module from module1.js

import {simpleString} from './module1.js';

console.log(simpleString);

2. Importing multiple named exports:


// This is module2.js, import multiple values into this module from module1.js

import {simpleString, simpleFunction, simpleNumber, myFunction} from './module1.js';

console.log(simpleString, simpleFunction, simpleNumber, myFunction);

3. Importing multiple named exports using an import namespace (i.e. importing an entire module):


// This is module2.js, import all values into this module from module1.js

import * as m1 from './module1.js';

console.log(m1.simpleString, m1.simpleFunction, m1.simpleNumber, m1.myClass, m1.myFunction);

4. Importing multiple named exports, renamed:


// This is module2.js, import renamed values into this module from module1.js

import {simpleString as sS, myClass as mF } from './module1.js';

console.log(sS, mF);

10.3 : Importing and Exporting a Default Value

A module can export one default value using the default keyword in combination with the export keyword.

Two styles are available when exporting a default value from a module:

1. Inline default exporting:


// moduleA.js
// Of course only one default can be used per module.
// Each export below would not live in the same file, it would throw an error
// Consider each line below to be in its own module

// literal values (no const or let used)
export default 'string';
export default 5;
export default true;
export default {};
export default [];

// expressions
const simpleString = 'simpleString';
const simpleNumber = 5;
export default simpleString;
export default simpleNumber;
export default (() => {});

// declarations
export default class myClass {} // semicolon optional with this style
export default function myFunction(){} // semicolon optional with this style

// unnamed declarations
export default class {} // semicolon optional with this style
export default function(){} // semicolon optional with this style

2. Renamed to default exporting


// moduleA.js

const simpleString = 'simpleString'; // No default keyword here

export {simpleString as default}; // Exporting as default

One style is available when importing the above default exports into another module:

1. Renamed default


// This is moduleB.js, import default from moduleA.js

// assumes you know that moduleA.js exports one default value
// the default is imported and renamed where it is imported
import nameGivenHereToDefault from './moduleA.js';

console.log(nameGivenHereToDefault);

Notes:

  1. In the end, default exports are just values, renamed to the word default. For this reason many avoid default exports and just use named exports.
  2. The word default, just like the word new, can't be used as a variable name.

10.4 : Combining a Default and Named Exports When Exporting and Importing

Modules can export both a single default value, along with other named values:


// moduleA.js
const myString = 'myString';
export default myString;
const myFunction = () => { };
export const myNumber = 5;

export { myFunction };

All the values export from moduleA.js above can be imported together using the following two styles:

By name:


// moduleB.js

import nameGivenHereToDefault, {myFunction, myNumber} from './moduleA.js';

console.log(nameGivenHereToDefault, myFunction, myNumber);

Using a single namespace:


// moduleB.js

import * as namespace from './moduleA.js';

console.log(namepsace.default, namespace.myFunction, namespace.myNumber);

10.5 : Imported Values Are Live References, Not Copies

ES modules do not create copied values when importing values. Instead, ES modules create pointers/references to live values.

In the code example below the counter and addToCounter() values are imported into module1.js, from module2.js. From module1.js I call the addToCounter() function, imported from module2.js, which changes the value of counter in module2.js and thus module1.js also. This is because the values imported are live values not copies of values.


<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
</head>

<body>

    <!--  Entry module is module1.js, dependency graph starts here -->
    <script type="module" src="./module1.js"></script>
    <!--  module1.js imports and uses module2.js -->

    <!-- module1.js
        import { counter, addToCounter } from "./module2.js"; 
        console.log(counter); // logs 0 
        addToCounter(10); 
        console.log(counter); // logs 10


        // Note, can't do this:
        // counter = 10;
    -->

    <!-- module2.js
        export let counter = 0; 
        
        export const addToCounter = function(amountToAdd) { 
            counter = counter + amountToAdd; 
        };

        export const foo = "foo";    
    -->

</body>
</html>
    

Keep in mind that imported values can't directly be changed. In other words, in the code example above directly changing the counter value from module1 will throw an error.

Notes:

  1. In contrast to ES modules, CommonJS modules import copied values instead of live read-only values.

10.6 : Re-exporting imports

While not commonly done it's possible to re-export, imported values, using the URL string specifier. When re-exporting, the values never enter the scope of the module that is re-exporting them. Values simply pass through to the scope where they are not re-exported.

Re-exporting imported values can be done using the following styles:

Single namespace:


export * from 'myModule.js';
    

Named values:


export {value1FromMyModule, value2FromMyModule} from 'myModule.js';
    

Re-named values:


export {value1FromMyModule as renamed1, value2FromMyModule as renamed2} from 'myModule.js';
    

As a default value:


export {default} from 'myModule.js';

// or

export {value1FromMyModule as default} from 'myModule.js';