Sponsored by
Frontend Masters,
advancing your skills with in-depth, modern front-end engineering
courses
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.
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.
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.
Contribute content, suggestions, and fixes on github:
https://github.com/FrontendMasters/javascript-enlightenment
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).
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.
String
MethodThe 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:
Array
Static MethodsES5 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:
Array.isArray()
method differs from using [] instanceof Array
only slightly when dealing with iframes.isArray()
method also respects values that are constructed from constructors extended from the native Array
constructor using the new class extends
keyword.Array
MethodsES5 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,2,,,,,,,,,3]
).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. 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:
get
and set
syntax is a shortcut for using Object.defineProperty()
and
Object.defineProperties()
to add the get
and set
property descriptors.Object
Static MethodsObject.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:
=
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).Object.getOwnPropertyDescriptors()
static method. This method returns an object containing all the own property descriptors for a given object.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:
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:
Object.preventExtensions()
: Stops properties from being added but not deleted.Object.seal()
: Stops properties from being added or configured (i.e. the configurable
descriptor attribute
for each property is changed to false
).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:
Object.isExtensible()
: Boolean check if an object is extensible.Object.isSealed()
: Boolean checking if an object is sealed.Object.isFrozen()
: Boolean checking if an object is frozen.bind()
Function MethodBefore 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:
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.use strict
ModeAdding, '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.
JSON
methodsJSON.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:
stringify()
and parse()
have an optional second function
parameter
that can be used to augment the result before it is returned.Trailing commas in Object
literals are now ok:
var myObject = {
name: 'Bill',
age: 12, // no syntax error
}
Notes:
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'
}
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:
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.
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:
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.
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:
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:
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:
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:
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.
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
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:
''.matchAll()
, ''.trimStart(); ''.trimLeft();
,
and
''.trimEnd(); ''.trimRight();
are currently at
stage 3.
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:
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]
*/
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:
[].keys()
, [].values()
, and [].entries()
array
methods
are very similar to the [].keys()
, [].values()
, and [].entries()
found
on the Map()
and Set()
values..next()
method.
.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:
[].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:
[].fill()
will accept negative
numbers
indicating a range which starts or counts from the end not the beginning.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:
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:
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:
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.
Object.assign()
is commonly used to merge objects together or create shallow clones of objects.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).
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:
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;
).
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:
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:
undefined
, not simply a falsely
value
like
null
or ''
.
arguments
array, available within the scope of a function, is not affected
by
default parameters values in any way.
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:
{a, b} = {a: 1, b: 2};
will throw an error but ({a, b} = {a: 1, b: 2});
will not).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:
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('');
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:
.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:
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
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.
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:
`\${}`
becomes
'${}'
.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:
String.Raw()
.
This tagged function ignores backslashes and returns the raw characters contained in the
template
literal (e.g.
String.raw`\${}`
returns
'\${}'
).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:
(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 });
.let add = (x,y) => x + y
)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:
const [a, ...b,] = [1, 2, 3];
const func = (...p,) => {};
SyntaxError
. 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:
break
and
continue
.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:
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;
},
};
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:
This chapter will cover usages for the new Map()
and Set()
objects.
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:
clear()
,
entries()
,
forEach()
,
get()
,
has()
,
keys()
, set()
, values()
)..set()
method returns the Map. Thus, .set()
can be chained
(i.e.
myMap.set(key,value).add(key,value);
).[...myMap].filter(...);
).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.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:
.add()
method returns the Set. Thus, .add()
can be chained
(i.e.
mySet.add('one').add('two');
).[...mySet].filter(...);
).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.This chapter will discuss the ES2015 class
syntactical sugar which conceals
JavaScripts
clunky object inheritance model.
class
Syntax ConcealsES2105 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:
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).
class
object. Which is the only
option!constructor
function, method functions, or static
method
functions in the class object.prototype
boilerplate is eliminated and referencing an inherited class
is
trivial using super
.class
Expression v.s. class
DeclarationA 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:
new
keyword.Human.prototype
) is
read-only.constructor
MethodEach 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:
constructor(){}
).super()
, within the constructor
is used to call the
parent
class (i.e. the class, a class inherits or is extend
'ed from).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:
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).
class MyClass { get MyMethod(){...} set MyMethod(x){...} }
).['method'+'Name'](){ ... })
.static
Class MethodWhile 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:
this.staticMethodName
.
But inside a constructor or class method you will have to either use ClassName.staticMethodName
or
this.constructor.staticMethodName
.extend
to Inherit Methods from Another ClassA 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:
Human
from Developer
using
the keyword super
.super()
with the expected arguments to
call
the parent's constructor.
super
to Call the Inherited ConstructorThe 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:
super
keyword should be the first expression in a constructor function
(i.e.
before the this
keyword is used). super
to reference Inherited MethodsThe 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"
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:
This chapter will examine the need for JavaScript Promises and then explain their usage.
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:
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:
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:
Promise()
is called before the Promise constructor returns the created object.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"
}
);
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:
Promise.resolve()
static method is typically used to wrap a Promise around a value.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:
then()
a resolve function or provide a non-function, no error will occur. The next then()
resolve function is passed an undefined
value.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:
catch()
method is typically used over providing each then()
with a rejection parameter.catch()
over providing a rejection function for each then()
.catch()
method is simply a short-hand for, .then(null, error => { // do something with error })
.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:
finally()
method was added in ES2018.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
});
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
});
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.
async
/await
syntax is not a replacement for promisesIt 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!
async
functions and the await
operatorUsing 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:
then()
not a long chain of them).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.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:
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 () => {}
).await
keyword only works inside a async
function and is used to pause a function until a promise is fulfilled.Object.getPrototypeOf(async function(){}).constructor
.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
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:
await
'ed for is rejected the await expression throws the rejected value.await
operator is sequentialIt 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
})()
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.
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:
import
).export
).All of the above requirements as of today have been satisfied by:
import
and export
)<script type="module" src="myModule.js" />
)Notes:
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:
<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:
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.<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:
type="module"
attribute is required so the browser will use the
native browser module loading API to parse/load each module.<script>
has its own entry file and dependency
graph. src
attribute.export
'ed are scoped only to that
module,
these values
don't leak into the
global scope.import
'ed.window
context. However, the use of the keyword
this
will not work inside of a module to reference the global runtime scope.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).<script type="module">
's' use defer
by default.Notes:
<script type="module">
:
Edge 17+, Firefox 61+, Chrome 63+, Safair
11.1+, iOS Safari 11.2+, Chrome Android 69+./
, ./
,
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..js
can be omitted from the URL specifier string.import value from 'https://otherdomain.com/modules/module1.js';
).import('module1.js')
). 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" }
).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:
/
,
./
, or ../
.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:
<script type="module" />
).import _ from 'lodash';
).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.
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:
export
and default
:
// 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';
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):
// 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:
/
, ./
, 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..js
can be omitted from the URL specifier string.import()
(i.e. don't think you can place imports in conditional
statements without using a dynamic import()
).strict mode
by default.window
) but not using this
value. The global scope has to be reference with a point to the global scope (e.g. window
).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);
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:
default
. For this reason many avoid default exports and just use named exports.default
, just like the word new
, can't be used as a
variable name.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);
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:
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';