JavaScript (ES2015+) Enlightenment
source link: https://www.tuicool.com/articles/hit/v6v2Qjz
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Grokking Modern JavaScript, In The Wild
Written by Cody Lindley
Sponsored byFrontend Masters, advancing your skills with in-depth, modern front-end engineering courses
Today, tools like Babel have made it commonplace to see ES2015 , ES2016, ES2017, ES2018, and ES2019 language updates/proposals in babelified source code. These compounding language changes can make it difficult to learn something like React, Apollo GraphQL, or Webpack.
This book aims to alleviate this problem by providing a curated selection of the commonly used language updates, tersely explain, to lessen this indirection. Thus, after studying the material in this book grokking new JavaScript code while learning JavaScript frameworks and tools, should be much more comfortable.
Written For:
The contents of this book are for developers who are working in a codebase using modern React, Vue, or Angular code and find recent JavaScript language updates/proposals to be causing too much indirection. And or, developers who want to drill into memory the latest and most commonly used JavaScript updates.
ES2015+ Enlightenment is not a rudimentary read on the JavaScript language. The content in this book attempts to take a developer with ES3 and ES5 knowledge and make them more knowledgeable about ES2015+ and the implications of modern changes to the language on JavaScript tools and frameworks.
How to Use/Read This Book:
First off, this is a mix between a book, a reference, and cheatsheet. My intention in writing is to shine a light on ES5+ language changes in a tersely and helpfully format. To be clear this is not a long form book on the JavaScript language. Or, a detailed reference. Consider this an elaborate cheatsheet with runnable code purposefully curated for those who know ES3 but need to master ES5+.
Second, this is a web book. A lot of contexts can be gained by just clicking on links in this book. If you ever feel in need of more context use the links in the text.
How to Use/Read The Code Examples:
Try and view the code examples as an extension of the words. First, read and re-read the words. Then read the code, especially the code comments, from top to bottom as if they are part of the surrounding paragraphs. The goal should be to grok the code until no questions remain as to what the code example is doing and expressing.
While Using/Reading the book remember, by design:
- The words and code comments are intentionally terse with the goal of code comprehension long-winded and exhaustive explanations.
- The code examples are contrived to reveal the nature of the code. Focus on what the code is doing and making sure you understand it, potentially over my words.
- The book is a mix of a mini book, a reference, and cheatsheet. Expect it to feel like one of these or all of these at the same time.
Chapter 1 : ECMAScript 5 (aka ES5) Recap
In this chapter, I'll recap the significant language updates introduce in ES5 to delineate these updates from the updates made in ES2015 (aka ES6).
1.1 : ES5 Browser and Node Compatibility
For the most part, ES5 is compatible with modern browsers (e.g. IE9+, excluding strict mode ) and Node since version 4.x.x.
Unless you have to support an older JavaScript engine/runtime (e.g. IE8) you are safe to assume most modern JavaScript engines/runtimes support ES5 .
1.2 : New ES5 String
Method
The ES5 .trim()
method removes whitespace from both ends of a string and creates a new
string.
var myString = ' Some Tabs and Spaces '; console.log(myString.length); // logs 28 var myNewString = myString.trim(); // trim it console.log(myNewString); // logs 'Some Tabs and Spaces' console.log(myNewString.length); // logs 20 // Note: this method does not mutate a value it creates a new value console.log(myString, myString.length); // This still is, ' Some Tabs and Spaces '
You should consider "Whitespace" to mean in general; spaces, tabs, and non-breaking spaces used in a string.
Specifically trim()
removes:
- \U0009 character tabulation
- \U000A line feed
- \U000B line tab
- \U000C form feed
- \U000D carriage return
- \U0020 space
- \U3000 ideographic space
- \UFEFF zero-width non-breaking space
1.3 : New ES5 Array
Static Methods
ES5 added the static Array
method, Array.isArray()
.
The Array.isArray()
method is used to determine precisely ( true
or false
)
if a value is a true Array
. In other words, this method checks to see if the provided value is an instance
of
the Array()
constructor.
console.log(Array.isArray([1,2,3])) //logs true // Note: does not work on Array-like objects console.log(Array.isArray({length: 3, 0:1, 1:2, 2:3})) //logs false
Notes:
-
The static
Array.isArray()
method differs from using[] instanceof Array
only slightly when dealing with iframes . -
This
isArray()
method also respects values that are constructed from constructors extended from the nativeArray
constructor using the new classextends
keyword.
1.4 : New ES5 Array
Methods
ES5 added the following Array
methods (i.e. higher-order iteration functions
):
[].some() [].every() [].filter() [].forEach() [].indexOf() [].lastIndexOf() [].map() [].reduce() [].reduceRight()
The [].some()
method will start testing values in array, until a test returns true
, then the function passed to .some()
immediately returns true
, otherwise the function returns false
(i.e. the first truthy value found will result in the function immediately returning true and potentially this could mean not all tests are run).
// Check if one or more items in the array is bigger than or equal to 2 var someMethod = [1, 2, 3].some(function(value, valueIndex, wholeArray){ return value >= 2; }); console.log(someMethod) // logs true because the array contains a value that is greater than or equal to 2
The [].every()
method will start testing values in array, until a test returns false
, then the function passed to .every()
immediately returns false
, otherwise the function returns true
(i.e. the first falsy value found will result in the function immediately returning false and potentially this could mean not all tests are run).
// Check if every item in the array is bigger than or equal to 2 var everyMethod = [1, 2, 3].every(function(value, valueIndex, wholeArray){ return value >= 2; }); console.log(everyMethod) // logs false because the array contains a value that is less than 2
The [].filter()
method will return a new Array
containing all the values that pass (i.e. are true) the filtering test
.
var myArray = [1,2,3]; // filter out any value in the array that is not bigger than or equal to 2 var FilteredArray = myArray.filter(function(value, valueIndex, wholeArray){ return value >= 2; }); console.log(FilteredArray) // logs [2,3] // Note: filter() returns a new Array, myArray is still equal to [1,2,3]
The [].forEach()
method executes a provided function for each value in the array.
// log to the console each value, valueIndex, and wholeArray passed to the function ['dog','cat','mouse'].forEach(function(value, valueIndex, wholeArray){ console.log('value = '+value+' valueIndex = '+valueIndex+' wholeArray = '+wholeArray); /** logs: "value=dog valueIndex=0 wholeArray=dog,cat,mouse " "value=cat valueIndex=1 wholeArray=dog,cat,mouse " "value=mouse valueIndex=2 wholeArray=dog,cat,mouse " **/ });
The [].indexOf()
method searches an array for the first
value matching the value passed to indexOf()
, and returns the index of this value.
// get index of first 'cat' console.log(['dog','cat','mouse', 'cat'].indexOf('cat')); // logs 1 // Note: Remember the index starts at 0
The [].lastIndexOf()
method searches an array for the last
value matching the value passed to [].lastIndexOf()
, and returns the index of this value.
// get index of last 'cat' console.log(['dog','cat','mouse', 'cat'].lastIndexOf('cat')); // logs 3 // Note: Remember the index starts at 0
The [].map()
method executes a provided function for each value in the array, and returns the
results
in a new array
.
var myArray = [5, 15, 25]; // add 10 to every number in the array var mappedArray = myArray.map(function(value, valueIndex, wholeArray){ return value + 10; }); console.log(mappedArray) // logs [15,25,35] // Note: map() returns a new Array, myArray is still equal to [5, 15, 25]
The [].reduce()
method runs a function that passes the return value to the next iteration of the
function
using values in the array from left to right
and returning a final value.
// add up numbers in array from left to right i.e. (((5+5) +5 ) + 2) var reduceMethod = [5, 5, 5, 2].reduce(function(accumulator, value, valueIndex, wholeArray){ return accumulator + value; }); console.log(reduceMethod) // logs 17 /** reduce also accepts a second parameter that sets the first accumulator value, instead of using the first value in the array. **/ // add up numbers in array from left to right, but start at 10 i.e. ((((10+5) +5 ) +5 ) + 2) var reduceMethod = [5, 5, 5, 2].reduce(function(accumulator, value, valueIndex, wholeArray){ return accumulator + value; // first iteration of func accumulator is 10 not 5 }, 10); console.log(reduceMethod) // logs 27
The [].reduceRight()
method runs a function that passes the return value to the next iteration of
the
function using values in the array from right to left
and returning a final value.
// add up numbers in array from left to right i.e. (((2+5) +5 ) + 5) var reduceRightMethod = [5, 5, 5, 2].reduceRight(function(accumulator, value, valueIndex, wholeArray){ return accumulator + value; }); console.log(reduceRightMethod) // logs 17 /** reduce also accepts a second parameter that sets the first accumulator value, instead of using the first value in the array. **/ // add up numbers in array from left to right, but start at 10 i.e. ((((10+2) + 5 ) +5 ) + 5) var reduceRightMethod = [5, 5, 5, 2].reduceRight(function(accumulator, value, valueIndex, wholeArray){ return accumulator + value; // first iteration of func accumulator is 10 not 5 }, 10); console.log(reduceRightMethod) // logs 27
Notes:
-
All the new methods ignore holes in arrays (i.e.
[1,2,,,,,,,,,3]
). -
All of these new
Array
methods, except forreduce
andreduceRight
accept a second parameter. This second parameter allows you to set thethis
value for the function being passed in the first parameter.
1.5 : New ES5 Getters and Setters (aka Accessors Descriptors or Computed Properties)
ES5 adds to Objects
computed properties via the keywords get
and set
.
This means that Objects
can have properties, that are methods, but don't act like
methods
(i.e you don't invoke them using ()
). In short by labeling a function in an object
with get
or set
one can invoke the backing property function on a property, by merely
accessing
the property, without using innovating brackets.
The example below demonstrates the nature of getter and setter properties:
var obj = { get RunsWhenAccessed(){ console.log('you accessed the property RunsWhenAccessed'); }, set RunsWhenSet(newValueBeingSet){ console.log('you set the property RunsWhenSet to : ' + newValueBeingSet); } } // access the RunsWhenAccessed property and the backing property function fires obj.RunsWhenAccessed; // logs 'you accessed the property RunsWhenAccessed' // access and set the RunsWhenSet property and the backing property function fires obj.RunsWhenSet = 'foo'; // logs 'you set the property RunsWhenSet to : foo' // note I am setting a value that becomes an argument and not calling a function using brackets
Don't over think getters and setters, they are simply a property who's value is determined by running a backing property function and the function is invoked by accessing or setting the property.
var person = { firstName : '', lastName : '', get name() { return this.firstName + ' ' + this.lastName; }, set name(str) { var n = str.split(/\s+/); this.firstName = n.shift(); this.lastName = n.join(' '); } } // set name, but store first and last separately person.name = 'Cody Lindley'; // get name, returns firstName and LastName combined console.log(person.name); // logs 'Cody Lindley'
Notes:
- The same property can have a getter and setter.
-
The
get
andset
syntax is a shortcut for usingObject.defineProperty()
andObject.defineProperties()
to add theget
andset
property descriptors. - A setter property can only take in a single value and thus a single argument is passed to the backing property function.
1.6 : New ES5 Object
Static Methods
Object.create() Object.getPrototypeOf() Object.defineProperty() Object.defineProperties() Object.getOwnPropertyDescriptor() Object.getOwnPropertyNames() Object.preventExtensions() Object.isExtensible() Object.sealed() Object.isSealed() Object.freeze() Object.isFrozen()
ES5 added the Object.create()
method so objects could be created and their prototypes
easily
setup. Object.getPrototypeOf
() was added to easily get an objects prototype.
// setup an object to be used as the prototype to a newly created myObject object below var aPrototype = { foo: 'bar', getFoo: function(){ console.log(this.foo); } } // create a new myObject, setting the prototype of this new object to aPrototype var myObject = Object.create(aPrototype); // logs 'bar' because myObject uses aPrototype as its prototype, it inherits getFoo() myObject.getFoo(); // logs 'bar' // get a reference to the prototype of myObject, using getPrototypeOf() console.log(Object.getPrototypeOf(myObject) === aPrototype); //logs true
ES5 added Object.defineProperty()
, Object.defineProperties()
, and Object.getOwnPropertyDescriptor()
so
object properties can be precisely defined (using descriptors) and retrieved. Descriptors provide
an
attribute that describe a property in an object. The attributes (i.e descriptors) that each
property
can have are: configurable,
enumerable, value, writable, get, and set
.
// create an object with a property and value const myObject = { prop1: 'value1' } // get the default descriptors for the prop1 property in myObject. console.log(Object.getOwnPropertyDescriptor(myObject,'prop1')); /** the above console logs: [object Object] { configurable: true, enumerable: true, value: "value1", writable: true } Note that get and set are undefined by default **/ // add a property, 'value2' with descriptors to myObject using Object.defineProperty() Object.defineProperty(myObject, 'prop2', { value: 'value2', writable: true, enumerable: true, configurable: true, }); // get the descriptors for the prop2 property. console.log(Object.getOwnPropertyDescriptor(myObject,'prop2')); // add multiple properties ('prop3' & 'prop4') with // descriptors to myObject using Object.defineProperties() Object.defineProperties(myObject, { prop3: { enumerable: true, configurable: true, value: 'value3' }, prop4: { enumerable: true, configurable: true, // Note that value and write properties are not added when using the properties set and get set: (newValue) => { console.log('you set the property prop4 to : ' + newValue); }, get: () => { console.log('you accessed the property prop4'); return 'prop4'; // the get returns the value for prop4 unlike using, value: 'value4' } } }); // get the descriptors for the prop3 and prop4 properties console.log(Object.getOwnPropertyDescriptor(myObject,'prop3')); console.log(Object.getOwnPropertyDescriptor(myObject,'prop4')); // Note that prop4's value is based on get and set, not value console.log(myObject.prop4);
Notes:
-
Using
=
to assign an object a property and value is a similar routine but not exactly identical to usingObject.defineProperty()
andObject.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). -
ES2017 added the
Object.getOwnPropertyDescriptors()
static method. This method returns an object containing all the own property descriptors for a given object. -
Below are the descriptions/definitions of the attributes of a property that make up a
property
descriptor:
value : contains the property's value. writable : contains a boolean indicating whether the value of a property can be changed or written too. get : reference to the function that is called when a property is read. set : reference to the function that is called when a property is set to a value. configurable : contains a boolean indicating whether a property can have its attributes changed and deleted. enumerable : contains a boolean indicating if a property will show up on certain operations.
ES5 added Object.keys()
which returns an Array
of non-inherited-enumerable properties
of
a given object. ES5 added Object.getOwnPropertyNames()
which returns an Array
of
non-inherited
properties of a given object regardless of enumerability.
// Create an object var myObject = Object.create(null); // no prototype used // Add prop to object created myObject.myObjectProp1 = 1; // Define a property using defineProperty() Object.defineProperty(myObject, 'myObjectProp2', { enumerable: false, value: 2 }); // Use keys() to get Array of all non-inherited, enumerable properties console.log(Object.keys(myObject)); // logs ["myObjectProp1"] // Use getOwnPropertyNames to get Array of all non-inherited properties including non-enumerable console.log(Object.getOwnPropertyNames(myObject)); // logs ["myObjectProp1", "myObjectProp2"]
Notes:
-
ES2017 added
Object.values()
which returns an array of a given object's own enumerable property values andObject.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. theconfigurable
descriptor attribute for each property is changed tofalse
). -
Object.freeze()
: Stops properties from being added, configured, or writable (i.e. theconfigurable
andwritable
descriptor attribute for each property is changed tofalse
)
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() Object.isSealed() Object.isFrozen()
1.7 : New ES5 bind()
Function Method
Before ES5 functions could only be invoked and given a this
value at innovation 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:
-
Don't forget that the main difference between
apply()
andcall()
is thatapply()
takes an Array of arguments passed to the called function whilecall()
takes a list of individual arguments (e.g. arg2, arg3, ... ).Bind()
also takes a list of individual arguments (e.g. arg2, arg3, ... ) passed to the new function being called.
1.8 : New ES5 use strict
Mode
Adding, 'use strict'
to the top of a JavaScript file or as the first line of a function
body
will change the language to a stricter
version of
JavaScript.
Today, using strict mode isn't typically a decision to be made because ECMAScript modules are implicitly
in
strict mode. In other words, spinning up a version of say, create-react-app
which uses ECMAScript modules
will
have 'use strict'
in play by default due to the fact that ECMAScript modules (i.e. import React from 'react';
) uses
strict
mode implicitly
. It is important you are aware of this fact. In other words, JavaScript
modules
give you strict mode by default.
1.9 : New ES5 JSON
methods
JSON.parse()
takes a JSON string and returns the JavaScript value(s) described by the
string.
In other words, JSON.parse()
will convert a string of JavaScript in JSON format
into real JavaScript values (i.e. Objects, Arrays,
Strings,
Numbers, Booleans etc...).
var JSONValues = JSON.parse('{"name":"Bill","age":22}') // convert JSON string to JS values console.log(typeof JSONValues); // logs "object" console.log(JSONValues.name, JSONValues.age); // logs Bill, 22
JSON.stringify()
takes JavaScript values and returns a string representing the values.
var JSONString = JSON.stringify({ name: 'Bill', age: 2 }); // Convert JS Object to JSON String console.log(typeof JSONString) // logs "string" console.log(JSONString) // logs "{ "name": "Bill", "age":2} "
Notes:
-
Both
stringify()
andparse()
have an optional second function parameter that can be used to augment the result before it is returned.
1.10 : New ES5 Syntax Changes
Trailing commas in Object
literals are now ok:
var myObject = { name: 'Bill', age: 12, // no syntax error }
Notes:
- Be aware, trailing commas are not allowed in JSON.
- ES2017 will allow trailing commas when defining function parameters or calling a function with arguments. However, calling a function with a comma alone or defining a function parameter as a comma alone will throw a SyntaxError.
Reserved words can now be used as unquoted Object
property keys:
// no syntax error when using reserved keywords as property/keys on an object var myObject = { new: 'new', class: 'class', if: 'if', function: 'function' }
1.11 : From ES5 to ES2015. What?
ES2015 was first called ES6 because at the time an update to the 5th edition of ECMAScript would logically be title ECMAScript edition 6 or "ES6". However, a naming tweak for language updates/changes occurred in 2015. It was decided by TC39 , the standardization group for JavaScript/ECMA-262, to release stage four proposals once a year (i.e. stage four are approved changes to the language). Given this change, new updates to the language moving forward would be given the titles ES2015 (i.e. ES6), ES2016, ES2017, ES2018 etc... . Basically, language changes/updates are semantically titled under the year in which the update/change becomes standardized.
Notes:
- If it is not obvious, it should be noted that just because a JavaScript language update/change has been standardized does not mean those who make use of the ECMAScript standard will implement the updates/change (i.e., adoption of new standards is a slow and often complicated affair e.g., browser compatibility).
Chapter 2 : Running ES2015+ (Compatibility, Compiling, and Polyfills)
Writing and running ES2015+ (i.e. ES2015, ES2016, ES2017, ES2018) code and staged proposals is not as simple as writing some code and then having a web browser or Node run it. To run ES2015+ and staged proposals a pre-compiler and polyfills are needed. This chapter digs into some of these details.
2.1 : ES2015 Native Runtime Compatibility
Native support for ES2015+ (i.e. ES2015 , ES2016 , ES2017 , ES2018 etc...) varies greatly depending upon which JavaScript engine and runtime one is needing to support (e.g. V8 & Node, V8 & Chrome, JavaScriptCore & Safari, SpiderMonkey & Firefox etc..).
In short, both modern day Node and modern web browser engines mostly have full support for ES2015 (starting with Node 6.14.4+, Edge 15, Chrome 47, Safari 10, and Firefox 54). However, things get complicated in terms of compatibility for ES2016+ and staged proposals . This is why a lot of developers side-step chasing compatibility and turn to polyfills and compilers. Polyfills plug JavaScript runtimes environments, at runtime, with newer unsupported API's while a compiler will transform newer unsupported syntax to previous versions of JavaScript (i.e. ES2015+ > ES5). This combination of polyfills and compiling allows developers to write ES2015+ JavaScript today while still supporting current and previous runtimes (i.e. Write new 2015+ JavaScript, transforming it using something like Babel, then it can run it in IE9 with polyfills).
Notes:
- The staging of JavaScript proposals/updates is the process that allows JavaScript to change over time. A proposed change to the language starts at stage 0 and is finished when it reaches stage 4 (stages: 0 = strawman, 1 = proposal, 2 = draft, 3 = Candidate, 4 = finished ). When a proposal is finished it simply means it is ready to be added to the formal specification. Today, staged proposals regardless of if they have been officially added to the JavaScript specification can be adopted prematurely by developers using polyfills and compilers (e.g. Both TypeScript and Babel can be configured to interpret staged proposals).
2.2 : Running ES2015+ Using Online Tools
The simplest way to run ES2015+ code online (including staged proposals) is to use the online Babel REPL .
If you'd like to run ES2015+ code in the context of the web platform try the codesandbox.io tool. The vanilla sandbox uses Parcel which uses Babel and babel-preset-env by default. Most of the code examples in this book can be run in codesandbox.io by clicking on the "run/edit in codesandbox.io" link above the code.
2.3 : Running ES2015+ locally
A common way to run ES2015+ code on your local computer, if you are already familiar with Node.js and REPL's, is to use babel-node . Babel-node is a node CLI tool that will compile ES2015+ syntax to ES5 syntax before running it. It can be used in place of the Node.js CLI to run JavaScript code using the Node runtime. Keep in mind that most of ES2015 has been supported in Node since 6.14.4+ . But if you want ES2015+ including staged proposals you'll need to use a tool like babel-node .
Personally, I prefer using Quokka.js in my code editor with Babel enabled . I setup Quokka to use babel-preset-env and stage 1-3 .
Notes:
- Writing source code that uses ES2015+ in a coding environment isn't the topic of this book. However, keep in mind, that most developers today working on the front-end will set up a compiler like Babel or TypeScript as part of a development environment process so ES2015+ code will be compiled as it is developed. Typically, this involves using a module bundler that makes use of Babel or Typescript during development and production bundling.
- Babel Polyfill is automatically loaded when using babel-node.
2.4 : Compiling ES2015+ Development Syntax To Static ES5 Production Syntax
Because developers today don't want to wait for native support for newer ES2016 , ES2017 , ES2018 syntax and coming syntax proposals they will adopt a compiling step for JavaScript source code. A compiling step takes ES2015+ syntax, and potentially staged syntax proposals , and transforms newer/proposed syntax to ES5 syntax ( polyfill require for complete compatibility). For example, it can take arrow function syntax and convert it to ES5 syntax.
// Compilers like Babel/TypeScript will take this: const myFunction = () => {}; // And turn it into this: var myFunction = function myFunction() {};
The most common compilers in use today are Babel and TypeScript . These compilers are routinely used as part of a build/bundling module process where ES2015+ source code and ES modules syntax are taken in and transformed to static ES5 production code (e.g. Webpack and Parcel exist to bundle assets, but they can also compile ES2015+ code found in JavaScript modules to ES5 when bundling).
Notes:
- The Babel compiling tool does double duty by compiling not just ES2015+ to ES5 but also things like JSX to ES5 and Flow to ES5 using plugins .
- Compiling does not come without caveats .
- Babel and TypeScript differ in the fact that Babel is not trying to be a superset of JavaScript. In other words, Babel does not exist so non-standard language features can be bolted on to the language. However, TypeScript views non-standard language updates as a core part of its purpose for existing (e.g. built in static type checking).
2.5 : Compiling ES2015+ Syntax Dynamically at Runtime
Compilers like Babel are typically used by tools like Webpack or Parcel during module bundling to create static files that are then used in production. However, Babel can also be used dynamically at runtime via babel-standalone . Below is a example of using babel-standalone in a web browser via a .html document.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> <div id="output"></div> <!-- Load ES2015+ Polyfill, core-js used by Babel polyfill --> <script src="https://unpkg.com/[email protected]/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:
- Sites like JSFiddle, JS Bin, the REPL on the Babel site, etc. These sites compile user-provided JavaScript in real-time.
- Apps that embed a JavaScript engine such as V8 directly, and want to use Babel for compilation
- Apps that want to use JavaScript as a scripting language for extending the app itself, including all the goodies that ES2015 provides.
- Integration of Babel into a non-Node.js environment (ReactJS.NET, ruby-babel-transpiler, php-babel-transpiler, etc).
2.6 : Polyfill'ing JavaScript API's at Runtime
Polyfills are JavaScript code used to plug or fill runtime environments with newer non-syntax parts of the
JavaScript
language that it may be lacking. Below is an example of an Object.assign()
polyfill that will check to see if the JavaScript runtime has Object.assign()
and if not will add it to the runtime.
// if Object.assign is missing then polyfill it. if (typeof Object.assign != 'function') { // Must be writable: true, enumerable: false, configurable: true Object.defineProperty(Object, "assign", { value: function assign(target, varArgs) { // .length of function is 2 'use strict'; if (target == null) { // TypeError if undefined or null throw new TypeError('Cannot convert undefined or null to object'); } var to = Object(target); for (var index = 1; index < arguments.length; index++) { var nextSource = arguments[index]; if (nextSource != null) { // Skip over if undefined or null for (var nextKey in nextSource) { // Avoid bugs when hasOwnProperty is shadowed if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { to[nextKey] = nextSource[nextKey]; } } } } return to; }, writable: true, configurable: true }); }
Basically, JavaScript polyfills fill in newer missing API features if they are missing from the runtime. The babel-polyfill documentation offers the following explanation for polyfill'ing:
"You can use new built-ins like Promise or WeakMap, static methods like Array.from or Object.assign, instance methods like Array.prototype.includes, and generator functions (provided you use the regenerator plugin). The polyfill adds to the global scope as well as native prototypes like String in order to do this." - Babel Docs
Polyfills are typically used in conjunction
with
compiling tools like Babel
to offer full compatibility for both syntax and newer API
features
(i.e. syntax meaning something like: () => {}
and API meaning something like Object.assign()
).
This is why Babel provides the babel-polyfill
.
Notes:
- The polyfills used by Babel can be also used as standalone solutions with Node and Web browsers (i.e. regenerator runtime and core-js ).
- Tools like polyfill.io are available for polyfill'ing browsers with not just JavaScript API updates but also with browser/web API updates as well (e.g. requestAnimationFrame ).
Chapter 3 : New ES2015+ Methods
In this chapter, I'll break down the newest methods from ES2015+. When targeting an ES5 only runtime (i.e. IE9) all of these methods have to be polyfilled .
3.1 : New Number
Static Method (ES2015)
ES2015 added the Number.isInteger()
method that will return true
if the
value
passed to it is an integer (i.e. a number with no fractional part). Otherwise, it will return false
.
console.log(Number.isInteger(0)); // logs true console.log(Number.isInteger(1)); // logs true console.log(Number.isInteger(0.1)); // logs false, has a fractional part console.log(Number.isInteger(Infinity)); // logs false console.log(Number.isInteger([1])); // logs false
3.2 : New String
Methods (ES2015, ES2017)
-
''.startsWith()
ES2015 -
''.endsWith()
ES2015 -
''.includes()
ES2015 -
''.repeat()
ES2015 -
''.padStart()
ES2017 -
''.padEnd()
ES2017 -
''.matchAll()
(coming soon, stage 3 proposal) -
''.trimStart()
/''.trimLeft()
(coming soon, stage 3 proposal) -
''.trimEnd()
/''.trimRight()
(coming soon, stage 3 proposal)
ES2015 added ''.startsWith()
and ''.endsWith()
. These methods can check if
a
string begins or ends with a specific sub-string.
// True or false does 'pre-funded' start with 'pre-' console.log('pre-funded'.startsWith('pre-')) // logs true // True or false does 'pre-funded' end with 'funded' console.log('pre-funded'.endsWith('funded')) // logs true
ES2015 added ''.includes()
. This method will check if a string contains a specific
sub-string.
// True or false does 'pre-funded' include the sub string 'fund' console.log('pre-funded'.includes('fund')) // logs true
ES2015 added ''.repeat()
. This method will take a string and return the same string
repeated
as many times as provided in the first argument.
// Take the string 'He' and return a string containing 'He' repeated three times console.log('He'.repeat(3)) // logs 'HeHeHe'
ES2017 added ''.padStart()
and ''.padEnd()
. These methods will pad the
beginning
or end of a string with repeating non-breaking spaces (the default) or a specified string of
characters.
When padding you supply the final length of the entire string. Including the original string. The
non-breaking
spaces or specified string will fill in any of the characters not taken up by the original string.
// Pad the start of 'GO!' with spaces so the total length of the string is 10, including 'GO!'. console.log('GO!'.padStart(10)) // logs ' GO!' // note how the ' ' repeated until a length of 10 was reached // Pad the start of 'GO!' with '.' so the total length of the string is 10, including 'GO!'. console.log('GO!'.padStart(10, '.')) // logs '.......GO!' // note how the '.' repeated until a length of 10 was reached // Pad the end of 'GO!' with spaces so the total length of the string is 10, including 'GO!'. console.log('GO!'.padEnd(10)) // logs 'GO! ' // note how the ' ' repeated until a length of 10 was reached // Pad the start of 'GO!' with '.' so the total length of the string is 10, including 'GO!'. console.log('GO!'.padEnd(10, '!')) // logs 'GO!!!!!!!!' // note how the '!' repeated until a length of 10 was reached
Notes:
-
The string methods
''.matchAll()
,''.trimStart(); ''.trimLeft();
, and''.trimEnd(); ''.trimRight();
are currently at stage 3 .
3.3 : New Array
Static Methods (ES2015)
Array.from() Array.of()
ES2015 added the Array.from()
static method. This method will take Array-like values
(objects
with a length property and indexed values) or iterable values and convert them into Array
values
(iterable
values are: String,
Array, TypedArray, Map, and Set
).
// Array from Array-like values const myArray1 = Array.from({length: 2, 0: 'zero', 1:'one'}); console.log(myArray1); // logs ["zero", "one"] // Array from String (i.e. an iterable) const myArray2 = Array.from('foo'); console.log(myArray2); // logs ["f", "o", "o"] // Array from an Array (i.e. an iterable) const myArray3 = Array.from([1, 2, 3]) console.log(myArray3); // logs [1, 2, 3], well that is silly // Array from an Array (i.e. an iterable), where each item in the array is run through a function // The second argument to .from() can be a function called on each iterable const myArray4 = Array.from([1, 2, 3], item => item * item) console.log(myArray4); // logs [1, 4, 9]
Notes:
-
Array.from()
creates a new, shallow-copied Array.
ES2015 added the Array.of()
static method. This method creates an Array
from arguments.
Unlike
the array constructor it can handle a case like Array.of(5)
resulting in [5]
while Array(5)
will result in [undefined, undefined, undefined, undefined, undefined]
.
// create an array containing 5 values console.log(Array.of(5,{},undefined,[],'string')); /* logs: [5, [object Object] { ... }, undefined, [], "string"] */ // create an array containing 5 undefined values console.log(Array(5)); // works if only one argument is passed /* logs: [undefined, undefined, undefined, undefined, undefined] */ // create an array containing 2 numeric values, 5 and 4 console.log(Array(5,4)); /* logs: [5, 4] */
3.4 : New Array
Methods (ES2015, ES2016)
[].findIndex() [].find() [].includes() [].keys() [].values() [].entries() [].copyWithin() [].fill() [].flat() [].flatMap()
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:
-
The
[].keys()
,[].values()
, and[].entries()
array methods are very similar to the[].keys()
,[].values()
, and[].entries()
found on theMap()
andSet()
values. -
An
iterator is basically an object with a
.next()
method. -
Don't confuse an iterator with an iterable. An iterator will keep track of what comes next
(i.e.
.next()
) while a iterable value is simply a value that comes with an interface for iterating over the value. An array is by default an iterable. But, methods like[].keys()
,[].values()
, and[].entries()
can be used to create an Array iterator object around the original array that will keep track of the current iteration and knows what the next item in the iteration is and how to get it.
ES2015 added the [].copyWithin()
method. This method shallow copies a range of items in
an
Array and then inserts the copies back into the same Array replacing items in the Array starting at
a
specific index.
let myArray1 = [1,1,1,1,5,5,5,5]; // copy from index 4 to 6 and take the copies and start replacing at index 0 console.log(myArray1.copyWithin(0, 4, 6)); //logs [5, 5, 1, 1, 5, 5, 5, 5] // i.e. this will replace the values at index 0 and index 1 with copied values 5, 5 let myArray2 = [1,1,1,1,5,5,5,5]; // passing negative numbers to any of the parameters means start from the end of the array // copy from -4 index to -2 and take the copies and start replacing at index 2 console.log(myArray2.copyWithin(-6, -4, -2)); //logs [1, 1, 5, 5, 5, 5, 5, 5] // i.e. this will replace the values at index 2 and index 3 with copied values 5, 5 let myArray3 = [1,1,1,1,5,5,5,5]; // copy from index 4 to end and take the copies and start replacing at index 0 console.log(myArray3.copyWithin(0, 4)); //logs [5, 5, 5, 5, 5, 5, 5, 5] // i.e. this will replace the values at index 0, 1, 2, 3 with copied values 5, 5, 5, 5
Notes:
-
The second and third arguments passed to
[].copyWithin()
will accept negative numbers indicating a range which starts or counts from the end not the beginning.
ES2015 added the [].fill()
method. This method will replace or fill a range of items
in
an Array with a new value.
// replace the values at index 0 and index 1 with 'foo' // the second and third arguments to fill() below say, start filling at index 0 and stop at index 2 console.log([5,5,5,5].fill('foo',0,2)) // logs ["foo", "foo", 5, 5] // replace all values from index 2 on with 'foo' console.log([5,5,5,5].fill('foo',2)) // logs [5, 5, "foo", "foo"] // replace all values with 'foo' console.log(Array(4).fill('foo')) // logs ["foo", "foo", "foo", "foo"]
Notes:
-
The second and third arguments passed to
[].fill()
will accept negative numbers indicating a range which starts or counts from the end not the beginning.
3.5 : New Object
Static Methods (ES2015, ES2017)
Object.is() Object.values() Object.entries() Object.assign() Object.fromEntries()
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 as1 === 1
, except for the following cases,Object.is( +0, -0 ) is false, while -0 === +0
is true andObject.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:
-
Don't forget
Object.keys()
was added in ES5.
ES2015 added the Object.assign()
static method. This method copies own enumerable
properties
from one object to a different target object.
// using assign() to clone an object const myObject = {'key1':'value1', 'key2':'value2'}; console.log(Object.assign({}, myObject)); /* logs new object, cloned from myObject [object Object] { key1: "value1", key2: "value2" } */ // using assign() to merge objects const myObject1 = {'key1':'value1', 'key2':'value2'}; const myObject2 = {'key3':'value3', 'key4':'value4'}; const myObject3 = {'key4':'4'}; console.log(Object.assign(myObject1, myObject2, myObject3)); /* logs a new object [object Object] { key1: "value1", key2: "value2", key3: "value3", key4: "4" } Note: same properties are overwritten. Last in to the parameter list wins. */ // using assign to coerce a string to an object console.log(Object.assign({},'foo')); /* logs a new object [object Object] { 0: "f", 1: "o", 2: "o" } */
Notes:
-
The
Object.assign()
won't deep clone reference values ; it will only deep clone the reference, not the value. This means that if you are cloning an object with reference values (e.g. objects inside of objects) the reference value is not cloned just the reference. Thus, deep cloning usingObject.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 . -
The
Object.assign()
is commonly used to merge objects together or create shallow clones of objects.
Chapter 4 : New ES2015+ Syntax
In this chapter, I'll break down the most used syntax updates from ES2015 and beyond. When targeting an ES5 only runtime these syntax updates have to be compiled from ES2015+ to ES5 (e.g., Babel > ES5).
4.1 : Using const
and let
(ES2015)
Before ES2015 variables were declared using the var
keyword. Today, it is more common
that
developers completely avoid the use of var
and instead use const
and let
when
needed. Const is used to declare variables with values that do not get reassigned. Reassigning a const
value throws an error. Additionally, both const
and let
honor
all block scope (i.e. { block scope }
), unlike var
which only honors function
block scope (i.e. function myFunction(){ block scoped }
).
This means const
and let
are safer because they don't leak out of things
like if
blocks or looping blocks. For example, in the code below the variables, doug
and MATH
are scoped to the if
block while jill
leaks out.
// MATH_CONSTANT and doug are scoped within the if(){ ... } blocks and unlike var they will not leak out. if(true){ let doug = 45; const MATH_CONSTANT = Math.PI; var jill = 44; // does not care about brackets } console.log(jill); // logs 44, because it leaked out of { } // try { console.log(doug); console.log(MATH_CONSTANT); }catch(e){ console.log('Can\'t find doug or MATH_CONSTANT'); }
Notes:
-
Keep in mind, using
const
does not create an immutable value, it just means that the variable can't be reassigned. Thus, changing the properties in something like anObject
or the values in anArray
that has been assigned to aconst
will not throw an error because it is not being reassigned (i.e. the reference/pointer to the value did not change thus no error).
Today expect to see code that never uses var
, favors const
, and uses let
only when
re-assignment is needed (i.e. let score = 0; score = 1;
v.s. const pie = 3.14;
).
4.2 : Using blocks to Create Scope (ES2015)
Before ES2015 if one needed a unique scope the only option was to use function scope:
// below doug and MATH_CONSTANT are only available with the scope of the function (function () { var doug = 45; var MATH_CONSTANT = Math.PI }()); try { console.log(doug); console.log(MATH_CONSTANT); }catch(e){ console.log('Can\'t find doug or MATH_CONSTANT'); }
Today, given that const
and let
remain scoped to blocks with no leaking developers can replace IIFE
with blocks if var
is
avoided.
So, don't be surprised if you see IIFE's replaced with simple blocks:
// doug and MATH_CONSTANT only available with the scope of the blocks { let doug = 45; const MATH_CONSTANT = Math.PI; } try { console.log(doug); console.log(MATH_CONSTANT); }catch(e){ console.log('Can\'t find doug or MATH_CONSTANT'); }
Notes:
- Keep in mind that the need to construct a private scope is being accomplished today with ES2015 modules. Modules have their own module scope by default. In other words, in modern code, ECMAScript modules are already scoped privately simply by creating the file. One does not need to create another private scope on top of that.
4.3 : Using Default Function Parameters (ES2015)
In the past, if a default value was needed for an argument passed to a function, boilerplate was needed:
const add = function(x, y) { x = x || 0; y = y || 0; return x + y; } console.log(add()) //logs 0
No longer is this the case. It is now possible to give parameters default values when defining a function. The code below is equivalent to the previous code.
const add = function(x = 0, y = 0) { return x + y; } console.log(add()) //logs 0
Notes:
-
Parameter default values are triggered by
undefined
, not simply a falsely value likenull
or''
. -
The
arguments
array, available within the scope of a function, is not affected by default parameters values in any way.
4.4 : Using Destructuring Assignments (ES2015)
Destructuring is a fancy word for unpacking elements from an array, or characters from a string, or properties from an object and assigning/reassigning them to one or more variables in a terse expression. Below are three code examples of each of these destructing expressions just mentioned.
1. Destructing 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. Destructing 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. Destructing 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:
-
Parentheses are required around object destructuring when the expression is assigning values (i.e.
{a, b} = {a: 1, b: 2};
will throw an error but({a, b} = {a: 1, b: 2});
will not).
4.5 : Using Destructuring With Function Arguments and Parameters (ES2015)
Take what you know about destructuring and now apply it to function arguments and parameters. Basically, a function parameter can be written as a collection of identifiers that can destructure String, Array, and Object values. The result is an extremely terse way to unpack Arrays, Objects, or Strings into separate parameters using a single argument.
Array arguments can be destructured:
// assign argument values to identifiers in the first parameter let func = function([one, two, three]) { console.log(one, two, three); } func([1,2,3]); // Same concept as: let [one, two, three] = [1,2,3]; but using arguments & parameters instead
Object arguments can be destructured (Most Common Usage):
// assign argument values to identifiers in the first parameter let func = function({ first: f, last: l }) { console.log(f,l); } func({ first: 'Bill', last: 'May' }); // Same concept as: let { first: f, last: l } = { first: 'Bill', last: 'May' };
String arguments can be destructured:
// assign argument values to identifiers in the first parameter let func = function([a, b, c]) { console.log(a,b,c); } func('foo'); // Same concept as: let [a, b, c] = "foo";
Destructuring function parameters are a terse way to pass complex data via a single argument and have the argument values be available in the function for immediate use without creating any assignment boilerplate in the function.
Notes:
- Destructuring can occur on any parameter.
4.6 : Using Default Destructuring Values (ES2015)
Destructuring syntax also permits the use of default values so that assignments can have fallback
values. In other words if the value you are destructuring is undefined
a fall back
value can be setup.
// Note: Fallback to default value when destructing 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 destructing 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 destructing Objects const { first: f = 'John', last: l = 'Doe' } = { first: 'Mary' }; console.log(f); // log Mary console.log(l); // log Doe
This will of course also work with when destructuring function arguments
// destructuring array argument, with default values const func1 = function([one=1, two=2, three=3]) { console.log(one, two, three); } func1([]); // destructuring object argument, with default values const func2 = function({ first: f = 'John', last: l = 'Doe' }) { console.log(f,l); } func2({}); // destructuring string argument, with default values const func3 = function([a = 'f', b = 'o', c = 'o']) { console.log(a,b,c); } func3('');
4.7 : Using the Spread Operator (ES2015, ES2018)
The spread operator (i.e. ...
) is used to unpack or expand elements from an Array,
properties
from an Object, or individual characters from a String, so as to immediately use these individual
values
in a couple of specific cases. Below I detail these cases.
1. Arrays can be spread into Array literals or when calling a function:
// spreading an Array into a function call console.log(...['f', 'o', 'o']); // logs f o o, like calling console.log('f', 'o', 'o'); // spreading Array(s) into an Array literal console.log([...[1, 2], ...[3, 4, 5]]); // logs [1,2,3,4,5]
Notes:
-
Spreading Arrays into Array literals is commonly used to clone Arrays, merge Arrays into a
new
or old Arrays, and spread Array elements into a function call as arguments. Spreading an
Array
can replaces usages of
.concat()
.
2. Object properties can be spread into Object literals:
const obj1 = {bob: true, steve: false}; const obj2 = {lisa: true, bob: false}; // spreading Objects into an object literal console.log({...obj1, ...obj2}); // logs {bob: false, steve: false, lisa: true} // Note that last bob value in wins.
Notes:
- Spreading objects was added in ES2018 .
- Order matters, last property and value spread wins.
-
Spreading Objects into object literals is commonly used to clone Objects, merge objects
into
a new or old Objects, or filling in defaults in Objects. Spreading an Object can replace
usages
of
Object.assign({}, obj1, obj2);
.
3. Strings can be spread into Array literals or when calling a function:
// spreading a string into an array console.log([...'bar']); // logs ['b','a','r'] // spreading a String into a function call console.log(...'foo'); // i.e. console.log('f','o','o'); // logs f o o
4.8 : Using the Rest Operator (ES2015, ES2018)
Unfortunately, the rest operator (i.e. ...
) looks exactly like the spread operator but instead of expanding
things
it will wrap things up.
The rest operator can be used in the following two cases:
1. When defining function parameters a rest operator can be used on the last parameter to indicate that any unidentified arguments should be wrapped up into an array.
/* By place the ... operator directly in front of the last function parameter the rest of the arguments passed to a function besides the ones before the rest parameter are wrapped up into an array. */ const func = function(param1, param2, ...restOfArguments) { console.log(param1, param2, restOfArguments); } // call func with 7 parameters, but only two are define, the rest are wrapped up into an array func(1,2,3,4,5,6,7); // logs 1 2 [3, 4, 5, 6, 7]
2. When destructing, remaining values can be wrapped up too:
// destructuring String characters in the variables a, b, c, and the rest into d Array const [a, b, c, ...d] = "doggy"; console.log(a); // d console.log(b); // o console.log(c); // o console.log(d); // ["g", "y"] // Note: use of Array of variables before assignment // // logs f o o an Array elements in the variables one, two, three, and the rest in restOfNumbers of Array const [one, two, three, ...restOfNumbers] = [1, 2, 3, 4, 5, 6]; console.log(one); // 1 console.log(two); // 2 console.log(three); // 3 console.log(restOfNumbers); // [4, 5, 6] // // logs f o o an Object properties in the variables f, l, and the rest in restOfProps Object const { first: f, last: l, ...restOfProps } = { first: 'Bill', last: 'May', age: 45, living: false }; console.log(f); // Bill console.log(l); // May console.log(restOfProps); // { age: 45, living: false }
Careful not to confuse the spread operator with the rest operator. The ...
operator is identical but the context in which it is used changes how the operator
functions.
The rest operator is used when defining function parameters and destructuring. The spread operator
turns
iterable items into arguments for functions, into properties for Object literals, or elements for
Array
literals.
4.9 : Using Template Literals (ES2015, ES2016, ES2018)
Characters wrapped in backticks (i.e. string here`
) instead of quotes are considered
template
string literals that return a String value. Template literals support line breaks and interpolation
(i.e.
a template like syntax for easily inserting values into the string, using ${}
).
In the code example below a multi-line string is created from the firstName
and lastName
variables
let firstName = 'Jane'; let lastName = 'Smith'; console.log(`Hello Mr. ${lastName}! Welcome! May I call you ${firstName}?`); // Note: when log to the console line breaks are honored /* "Hello Mr. Smith! Welcome! May I call you Jane?" */
Notes:
-
A backslash is used for escaping inside template literals (e.g.
\${}`
becomes'${}'
.
4.10 : Using Tagged Template Literals (ES2015)
Tagged template literals are template literals that get run through a function and passed the
template
literal details. Commonly a String
value is returned from a tag function, but any
value can be
returned.
In the code example below the tag function verifyName
will use a default name if one of
the
values passed to the template literal is undefined
. Basically, by tagging the template
literal with
a
tag function I make sure it will always have a first and last name even if the values passed to it
are
undefined.
// This is a contrived example highlighting the mechanics of a tag function let name; // tag function to adjust string if name is undefined const verifyName = function(stringsFromTemplate, nameTemplateData) { const s = stringsFromTemplate; // In an array if(nameTemplateData === undefined){ return `${s[0]}${'[no name given]'}${s[1]}${'[no name given]'}${s[2]}` }else{ return `${s[0]}${nameTemplateData}${s[1]}${nameTemplateData}${s[2]}` } } // Use verifyName tagged function with undefined value let greeting1 = verifyName`Hello ${name}! May I call you ${name}?` console.log(greeting1); //logs Hello [no name given]! May I call you [no name given]? name = 'Pat'; // Use verifyName tagged function with defined value let greeting2 = verifyName`Hello ${name}! May I call you ${name}?` console.log(greeting2); //logs Hello Pat! May I call you Pat?
Notes:
-
JavaScript provides one tag function baked into the language
String.Raw()
. This tagged function ignores backslashes and returns the raw characters contained in the template literal (e.g.String.raw`\${}`
returns'\${}'
).
4.11 : Using Fat Arrow Function Expressions (ES2015)
Today, developers have replaced traditional function expressions like:
const func = function (x) { return x * x; };
with a new syntax that uses a fat arrow (i.e. =>
):
const func = x => x * x ; // note: Omitted parenthesis around single parameter and use of implicit return. // This works fine when you have a single parameter and a single expression to return. // a less terse version of the above const func = (x) => { return x * x; }; // parenthesis and blocks only required if you have more than one parameter or expression const func = (x, y) => { const doubleY = y * 2; return z * doubleY; };
The previous code examples are similar in that they are function expressions, but the fat arrow function is not a total replacement for functions in general. Fat arrow function expressions are best suited for non-method functions, and they cannot be used as constructor functions. 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:
- "An Arrow Function does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment. " - http://www.ecma-international.org/ecma-262/6.0/#sec-arrow-function-definitions-runtime-semantics-evaluation
-
Forging the use of the blocks in an fat arrow function expression can require the use of
parentheses.
(e.g. const func = () => foo();
should be writtenconst func = () => (foo())
; andconst obj = x => { bar: x };
should be writtenconst obj = x => ({ bar: x });
.
4.12 : Using Trailing Commas (ES2015, ES2017)
Trailing commas in the code below are considered valid JavaScript today:
//Trailing comma in Array literal console.log([1, 2, 3,]) // no error //Trailing comma in Object literal console.log({bob: true, steve: false,}) // no error //Trailing comma in Function parameter definitions const func1 = ((x,) => { // no error console.log(x) })('cat'); //Trailing comma in Function arguments call const func2 = ((x) => { console.log(x) })('dog',); // no error // Trailing comma in array destructuring const [index0, index1,] = ['bee', 'flee']; // no error console.log(index0, index1); // Trailing comma in object destructuring const {bob:bobsValue, jill:jillsValue,} = {bob: false, jill: true}; // no error console.log(bobsValue,jillsValue);
Notes:
- Trailing commas are not allowed in JSON.
- Pragmatically trailing commas can be useful because you don't have to adjust commas anymore when adding new elements, parameters, or properties to JavaScript code. Additionally, not throwing an error on a trailing commas makes version control diffs cleaner and less troublesome.
-
Values using the rest operator may not have a trailing comma e.g. :
const [a, ...b,] = [1, 2, 3];
andconst func = (...p,) => {};
this will cause aSyntaxError
.
4.13 : Using the for-of loop (ES2015)
Commonly looping today will be done by way of an Array method like .map()
or .forEach()
. However, don't be surprised if you see the for-of loop in modern code. It
can
be used to loop over Strings, Arrays, Maps, Sets, basically anything that is an iterable
value.
The for-of loop goes through an iterable and assigns each entry in the iterable one at a time to the
variable(s)
define before the of
keyword.
Looping over Arrays using for-of loop:
const anArray = ['a', 'b', 'c']; for (const item of anArray) { console.log(item); // logs "a", "b", "c " }
Looping over Strings using for-of loop:
const aString = 'cat'; for (const character of aString) { console.log(character); // logs "c", "a", "t " }
Looping over Maps using for-of loop:
// create a new Map called myMap and preload it with key-value entries const myMap = new Map([ ['key1', 'value1'], ['key2', 'value2'] ]); // Use for-of loop to loop over Map for (const entry of myMap){ console.log(entry); // logs: // ["key1", "value1"] // ["key2", "value2"] }
As of ES2017 one can use Object.entries()
and destructing 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:
-
The for-of loop provides similar terseness found with Array looping methods but also
supports
break
andcontinue
.
4.14 : Using Shorthand Property Names (ES2015)
When defining an object literal if the colon and value are omitted the value from a similarly named
variable
within the same scope will be used. In the code below the const
myNumber
value is used for the myNumber
property value in the myObjectLiteral
object.
const myNumber = 1; const myObjectLiteral = { myNumber, // notice all I put here was a property name // the above is shorthand for myNumber : myNumber }; console.log(myObjectLiteral); // Logs Object {myNumber: 1}
Notes:
- Shorthand property names are often used in combination with destructuring.
4.15 : Using Shorthand Method Names (ES2015)
When adding methods (i.e. functions) to object literals a new terser syntax is possible.
This is the old non-short syntax:
const myObjectLiteral = { myMethod: function(parameters){ return; } };
This is the new shorthand syntax.
const myObjectLiteral = { myMethod(parameters){ return; } };
Which saves you from having to write, : function
.
Note how similar this new syntax is to getters and setters from ES5:
const myObjectLiteralWithGetterAndSetter = { set myMethod(parameters){ return; }, get myMethod(){ return; }, };
4.16 : Using Computed Property Names (ES2015)
Using brackets around property names/keys will permit the name/key to be a result of an expression.
const prop = 'prop'; // define an object literal with name/keys that are the result of an experssion const myObject = { // note property keys/names are computed [prop + 1]: 1, [prop + '2']: 2 } console.log(myObject); /* logs [object Object] { prop1: 1, prop2: 2 } */
Notes:
- This is similar to the bracket notation that can be used to access a key/property on an object from a computed property name.
Chapter 5 : ES2015 Map() and Set()
This chapter will cover usages for the new Map()
and Set()
objects.
5.1 : Using Maps instead of Object Literals (ES2015)
ES2015 comes with Map()
. Maps are key-value pairs stored in insertion order. Sounds a
lot like an
object right (i.e. {key1:value1, key2:value2}
)? Think of Maps as objects but with a built-in
native API for working with the object and its key-value pairs.
Examine the code below to gain a basic overview of the creation and usage of a Map
. Note that most
of
the built in methods for Map()
are demonstrated (e.g. clear()
, entries()
, forEach()
, get()
, has()
, keys()
, set()
, values()
).
// create a new Map called myMap // Preload myMap with key-value entries, as an Array, contained in an Array const myMap = new Map([ ['key1', 'value1'], ['key2', 'value2'] ]); // .forEach() // loop over each entry in the Map using Map's forEach() method myMap.forEach((value, key) => { console.log(`${key} = ${value}`); }); // logs "key1 = value1 " "key2 = value2 " // .entries() // List of all key-value pairs, spreading into console log console.log(...myMap.entries()) // logs ["key1", "value1"] ["key2", "value2"] // .keys() // List of all keys, spreading into console log console.log(...myMap.keys()) // logs "key1" "key2 " // .values() // List of all values, spreading into console log console.log(...myMap.values()) // logs "value1" "value2 " // .set() // Set a key-value pair myMap.set('key3', 'value3'); // .get() // Get a key-value pair console.log(myMap.get('key3')); // logs "value3" // .has() // Does a key have a value yet console.log(myMap.has('key3')); // logs true // .delete() //Delete a key-value pair, by key myMap.delete('key3') console.log(myMap.has('key3')) // logs false // .clear() //Clear all key-value pairs myMap.clear(); // .size() //Get current size of map (i.e. number of entires) console.log(myMap.size) // logs 0, because you just cleared the Map of entries
Don't forget when using a Map()
the key can be any value:
const myMap = new Map([ ['key1', 'some value for key 1'], [2, 2], [[1], [1,2,3]], [{id:1}, {prop1:'value',prop2:'value'}], ]); myMap.forEach( (value, key) => { console.log(`${key} = ${value}`); // logs: // // "key1=s ome value for key 1" // "2=2" // "1=1,2,3" // "[object Object]=[ object Object]" });
Notes:
- Until ES2015 Object literals we're the substitute for a Map. Obviously a real Map provides more features and out of the box methods for working with key-value pairs.
- Maps can be looped over using the for-of loop.
- The key and the value can both be any type of value, unlike Object literals in which the key is typically a string.
-
When should you use Maps over object literals? Use a Map when you are performing a lot of
work
on the entries and the built in methods make everything simpler (
clear()
,entries()
,forEach()
,get()
,has()
,keys()
,set()
,values()
). -
The
.set()
method returns the Map. Thus,.set()
can be chained (i.e.myMap.set(key,value).add(key,value);
). -
If you want to use Array methods on a Map simply spread the map into an array (i.e.
[...myMap].filter(...);
). -
A narrow version of
Map()
is also available calledweakMap()
. AweakMap()
, mainly differs from aMap()
in that it can only hold values that are objects and when a value is removed from aweakMap()
, and it has no other references, the value will immediately be garbage collected (aka weak references). Additionally aweakMap()
can't be iterated over, uses.length
to get size of the map, and only hasset()
,delete()
,get()
, andhas()
methods.
5.2 : Using Sets to create Arrays, with no Duplicates (ES2015)
ES2015 comes with Set()
. Sets are values stored in a specific order that can't contain
a duplicate.
Sounds
a bit like an Array object right (i.e. [value, value]
)? Think of Sets as Arrays but
with
a built in API for working with the items in the Array and the bonus of automatically eliminating
duplicates.
Examine the code below to gain a basic overview of the creation and usage of a Set. Note that most
of
the built in methods for Set()
are demonstrated (e.g. clear()
, entries()
, forEach()
, has()
, keys()
, add()
, values()
).
// create a new Set called mySet and preload it with values using an array/iterable // Note: that duplicates are removed when creating new Sets and adding values const mySet = new Set(['one', 'two', 'three', 'three']); // .forEach() // loop over each value in the Set using Set's forEach() method mySet.forEach((value) => console.log(value)); // logs "one" "two" "three" // .entries() // Note this uses each value as both the key and the value. // List of all value entries, spreading into console log console.log(...mySet.entries()) // logs ["one", "one"] ["two", "two"] etc... // .keys() // List of all keys (same func as values()), spreading into console log console.log(...mySet.keys()) // logs "one" "two" "three" // .values() // List of all values, spreading into console log console.log(...mySet.values()) // logs "one" "two" "three" // .has() // Does a Set have a specific value console.log(mySet.has('three')); // logs true // .add() // Add 'four' and 'three' to the Set. console.log(...mySet.add('three')); // logs "one" "two" "three" console.log(...mySet.add('four')); // logs "one" "two" "three" "four" // Note adding 'three' to a Set will not create a duplicate, as 'three' is a duplicate // .delete() //Delete a value mySet.delete('four') console.log(mySet.has('four')) // logs false // .clear() //Clear all ordered values mySet.clear(); // .size() //Get current size of Set (i.e. number of values) console.log(mySet.size) // logs 0, because you just cleared the Set of values
Notes:
-
The
.add()
method returns the Set. Thus,.add()
can be chained (i.e.mySet.add('one').add('two');
). -
If you want to use Array methods on a Set simply spread the set into an array (i.e.
[...mySet].filter(...);
). -
A narrow version of
Set()
is also available calledweakSet()
. AweakSet()
, mainly differs from aSet()
in that it can only hold values that are objects and when a value is removed from aweakSet()
, and it has no other references, the value will be garbage collected (aka weak references). Additionally aweakSet()
can't be iterated over, uses.length
to get size of the set, and only hasadd()
,delete()
, andhas()
methods.
Chapter 6 : ES2015 Class Syntax
This chapter will discuss the ES2015 class
syntactical sugar which conceals
JavaScripts
clunky object inheritance model.
6.1 : What class
Syntax Conceals
ES2105 class
syntax is a cleaner way to work with prototypal inheritance and object
factories.
The class
, extend
, constructor
, super
, and static
keywords
simplify and conceal what was already possible with JavaScript.
Before class
syntax was available working
with object constructors and prototypal inheritance, to mimic class's from other OOP languages,
was done in the following way:
// Create a Human class, i.e. an object factory for Human instances function Human(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } // Instances created from Human constructor have a fullName method Human.prototype.fullName = function () { return `${this.firstName} ${this.lastName}`; }; // Create a Developer class function Developer(firstName, lastName, type) { Human.call(this, firstName, lastName); this.type = type; } // Have Developer inherit from Human, by creating an object that inherits from Humans prototype Developer.prototype = Object.create(Human.prototype); // Make the constructor property point at Developer, not Human Developer.prototype.constructor = Developer; // Instances created from Developer constructor have a fullNameAndLanguage method Developer.prototype.fullNameAndLanguage = function () { return `${Human.prototype.fullName.call(this)} develops ${this.type}`; }; // Add static helper function to Developer Developer.isJSdev = function(cody){ return cody.language.toLowerCase() === 'javascript'; }; // Create an instance of Developer const cody = new Developer('Cody', 'Lindley', 'JavaScript'); // Call fullNameAndLanguage() method console.log(cody.fullNameAndLanguage()); // logs "Cody Lindley develops JavaScript" // Call fullName() method console.log(cody.fullName()); // logs "Cody Lindley" // Call static type of Developer console.log(Developer.isJSdev(cody)); // logs true
Using
ES2015 class
syntax
the above can be re-written like so:
// Create a Human class, i.e. an object factory class Human { constructor (firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } // Instances created from Human constructor have a fullName method fullName(){ return `${this.firstName} ${this.lastName}`; } } // Create a Developer class class Developer extends Human { // Have Developer inherit from Human constructor (firstName, lastName, type) { super(firstName, lastName); // call Human constructor but in this context this.type = type; } // Instances created from Developer constructor have a fullNameAndLanguage method fullNameAndLanguage(){ return `${super.fullName()} develops ${this.type}`; } // Add static helper function to Developer static isJSdev (cody){ return cody.language.toLowerCase() === 'javascript'; } } // Create an instance of Developer let cody = new Developer('Cody', 'Lindley', 'JavaScript'); // Call fullNameAndLanguage() method console.log(cody.fullNameAndLanguage()); // logs "Cody Lindley develops JavaScript" // Call fullName() method console.log(cody.fullName()); // logs "Cody Lindley" // Call static type of Developer console.log(Developer.isJSdev(cody)); // logs true
The details of a class
object will be broken down in this chapter. For now, you should
observe
from the newer class
syntax the following:
-
The use of the
class
,extend
,constructor
,super
, andstatic
keywords that were added to JavaScript to simplify prototypal inheritance for object-oriented minded developers (i.e. cleaner, concealing prototypal nuances, less boilerplate). -
The use of shorthand method names within the
class
object. Which is the only option! -
No commas separating the
constructor
function, method functions, orstatic
method functions in the class object. -
Notice how
prototype
boilerplate is eliminated and referencing an inherited class is trivial usingsuper
.
6.2 : The class
Expression v.s. class
Declaration
A class can be defined using either a class declaration or class expression.
A class declaration:
class Human { constructor (firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } // class name can be used to reference the class inside the class object classMethod (){ console.log(Human); // logs out class object backing property function } } const jill = new Human(); // call classMethod and log to the console reference to class, from inside of the class jill.classMethod();
A class expression:
const Human = class optionalClassNameForReferenceInClassMethods { constructor (firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } // the optional name can be used to reference the class inside the class object classMethod (){ // logs out class object backing property function console.log(optionalClassNameForReferenceInClassMethods); } } const jill = new Human(); // call classMethod and log to the console reference to class, from inside of the class jill.classMethod();
Notes:
new Human.prototype
6.3 : Using a Class constructor
Method
Each class
definition can have exactly one constructor
function that is
invoked
when an instance of the class is instantiated. Typically, the constructor
function
will
define the initial properties for instances created from the class.
// create a Human class that expects a first and last name value when instantiated class Human { constructor (firstName, lastName) { console.log('constructing'); // the this keyword, used below, is the new object create when calling Human this.firstName = firstName; // create a firstName property and give it a value this.lastName = lastName; // create a lastName property and give it a value } } const may = new Human('May','Jones'); // logs 'constructing' console.log(may); /* logs: [object Object] { firstName: "May", lastName: "Jones" } */
Notes:
-
A constructor function is optional. If you omit one, a blank constructor is created for you
(i.e.
constructor(){}
). -
The keyword
super()
, within theconstructor
is used to call the parent class (i.e. the class, a class inherits or isextend
'ed from).
6.4 : Using a Class Method
Class methods can be called on instances of the class. When called, the method of the class uses the
values
defined on the instance via the this
keyword.
// create a Human class that is passed a first and last name value when instantiated class Human { constructor (firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } // Instances created from Human class have a fullName class method fullName(){ // this inside a class method refers to the instance the method is called on return `${this.firstName} ${this.lastName}`; } } const may = new Human('May','Jones'); // may is an instance of the Human class const bill = new Human('Bill','Jones'); // bill is an instance of the Human class console.log(may.fullName()); // logs "May Jones" console.log(bill.fullName()); // logs "Bill Jones"
The fullName
class method is defined once for all instances.
Notes:
- Class methods are non-enumerable by default.
- Class methods are written using the shorthand method naming.
-
The keyword
super.[class method]
within a class method can reference and or call the methods of the parent class (i.e. the class, a class inherits or isextend
'ed from). -
Getters and Setters (aka Accessors descriptors or computed properties) can be used on class
methods
(i.e.
class MyClass { get MyMethod(){...} set MyMethod(x){...} }
). -
Computed property names can be used within class definitions (i.e.
['method'+'Name'](){ ... })
.
6.5 : Using a static
Class Method
While class methods are called on instances of a class, static
class methods are called
from
the class itself. Don't over think this setup. This is the different between something like Array.isArray()
and [].forEach()
. Array.isArray()
is a static method on the Array "class"
itself
while [].forEach()
is a "class" method called on instances of an Array
// Define a Human class with a static method class Human { static isHuman(classInstance){ // is the constructor of the classInstance the same as this class i.e. Human = Human, return classInstance.constructor === this; } } // create an instance of the Human class var pat = new Human(); // call static method on Human class Human.isHuman(pat); // logs true
Notes:
- Static methods are also inherited.
-
From within a static method, you can refer to other static methods with
this.staticMethodName
. But inside a constructor or class method you will have to either useClassName.staticMethodName
orthis.constructor.staticMethodName
.
6.6 : Using extend
to Inherit Methods from Another Class
A class can be sub-classed (i.e. Human > Developer) when using the extend
keyword
upon
definition. For example, in the code below when defining the Developer
class we use
the extend
keyword to link/inherit the parent Human
class to the child Developer
class.
// Create a Human class, i.e. an object factory class Human { constructor (firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } // Instances created from Human constructor have a fullName method fullName(){ return `${this.firstName} ${this.lastName}`; } } // Create a Developer class class Developer extends Human { constructor (firstName, lastName, type) { super(firstName, lastName); // like calling Human.call(this, firstName, lastName); this.type = type; } } // Create an instance of Developer let cody = new Developer('Cody', 'Lindley', 'JavaScript'); // Call fullName() method, inherited from Human Class console.log(cody.fullName()); // logs "Cody Lindley"
Notes:
- Built-in objects can be extended or sub-classed not just user-defined classes.
-
This inheritance link gives scope access to
Human
fromDeveloper
using the keywordsuper
. -
If a sub-class is missing a constructor, the parent constructor is called. If a sub-class
does
have a constructor, it will need to call
super()
with the expected arguments to call the parent's constructor.
6.7 : Using super
to Call the Inherited Constructor
The super
keyword within a constructor method is used to invoke the parent class from
the
child class. This invocation setups the needed properties for inherited methods to run correctly.
// Create a Human class, that other class's will use as a parent class class Human { constructor (firstName, lastName) { console.log('Super() called this constructor from child Class'); this.firstName = firstName; this.lastName = lastName; } // Instances created from Human constructor have a fullName method fullName(){ return `${this.firstName} ${this.lastName}`; } } // Create a Developer class, that is a child class of Human class Developer extends Human { constructor (firstName, lastName, type) { // super has to be used first, if used super(firstName, lastName); // like calling Human.call(this, firstName, lastName); // the above, using a parent constructor, does this: // this.firstName = firstName; // this.lastName = lastName; this.type = type; } } // Create a Lawyer class, that is a child class of Human class Lawyer extends Human { constructor (firstName, lastName, type) { // super has to be used first, if used super(firstName, lastName); // like calling Human.call(this, firstName, lastName); // the above, using a parent constructor, does this: // this.firstName = firstName; // this.lastName = lastName; this.type = type; } } // Create an instance of Developer, both the Developer and Human constructors are invoked. const cody = new Developer('Cody', 'Lindley', 'JavaScript'); console.log(cody.fullName()) // works because Developer has a firstName and lastName setup by calling super() // Create an instance of Lawyer, both the Lawyer and Human constructors are invoked. const lisa = new Lawyer('Lisa', 'Lindley', 'Criminal'); console.log(lisa.fullName()) // works because Lawyer has a firstName and lastName setup by calling super()
Notes:
-
The
super
keyword should be the first expression in a constructor function (i.e. before thethis
keyword is used).
6.8 : Using super
to reference Inherited Methods
The super
keyword when used within a class method or static method will be a reference
to
the parent class's methods.
class Human { constructor (firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } fullName(){ return `${this.firstName} ${this.lastName}`; } } class Developer extends Human { // Have Developer inherit from Human constructor (firstName, lastName, type) { super(firstName, lastName); this.type = type; } fullNameAndLanguage(){ // use super keyword to reference parent class method, and invoke it. return `${super.fullName()} develops ${this.type}`; // Note referencing super alone will throw an error i.e. console.log(super); } } // Create an instance of Developer const cody = new Developer('Cody', 'Lindley', 'JavaScript'); // Call fullNameAndLanguage() method console.log(cody.fullNameAndLanguage()); // logs "Cody Lindley develops JavaScript"
6.9 : Extending JavaScript Built-in's and Web API Constructors/Classes using class
Syntax.
Before ES2015 class syntax sub-classing/extending a built in constructor/class like Array()
had significant
limitations
.
Today, extending the JavaScript Array()
class is trivial using class syntax.
class MyCustomArray extends Array{ customEntriesMethod(){ return Object.entries(this); } } let myCustomArrayInstance = new MyCustomArray('one', 'two', 'three'); myCustomArrayInstance.push('four'); console.log(myCustomArrayInstance.customEntriesMethod()); // logs [["0", "one"], ["1", "two"], ["2", "three"], ["3", "four"]]
Even, JavaScript web API's like the DOM can be extended.
Notes:
- If you run the above code through Babel is will break. Sub-classing constructors/classes has to be supported natively. Transpiling and polyfilling can't help and in fact, will break the code. The environment either supports extending built-in classes/object or it does not. If you extend natives make sure you are not also babel'ifying/transpiling the code.
Chapter 7 : ES2015 Promises
This chapter will examine the need for JavaScript Promises and then explain their usage.
7.1 : Asynchronous Programming Basics
Code that does not run completely as part of a normal execution cycle is said to run asynchronously.
Asynchronously executed
code in this context basically means that one defines code now to occur later in the future while
not blocking other code from running synchronously. The
simplest
example of this is setTimeout()
.
// execute the passed function in 10'ish seconds in the future setTimeout(() => console.log('Ten seconds ago, you ask me to run this code'), 10000); // But keep executing code, don't wait for 10secs, and block all code execution console.log('I am not delayed by 10 seconds from running');
The function passed to setTimeout()
is known as an asynchronous callback function. This function will
run
10'ish seconds in the future without blocking other code from running. Consider that the setTimeout()
function argument is not all that different from a callback function for a click event or a callback function used to capture the response from an XMLHttpRequest
network request.
Basically, when you ask JavaScript to wait to run some code, while not blocking the rest of the program from running, you are dealing with asynchronous code.
Notes:
- An in-depth understanding of asynchronous programming in JavaScript requires an understanding of the call stack and the event-loop. Here is a simple and concise review of these parts, "Help, I'm stuck in an event-loop." . If you've struggled in the past with understanding promises it is likly because you lack the foundational information found in the aforementioned video.
7.2 : Asynchronous Programming Before ES2015 (Or, Why Promises?)
Callback functions before ES2015 were the typical means in which one dealt with asynchronous programming situations. And even today, a simple callback function works just fine in many situations (e.g. the user clicks on a button and the corresponding callback function is run asynchronously).
However, asynchronous situations do exits that make using callback functions buggy and laborious. These situations can involve complex network exchanges found between a client and a server. Imagine you need to make five different requests to the server from the client and the requests have a complex relationship with each other. For example, imagine that the first request has to finish before the second and third request can begin. And both the second and third request have to finish before the fourth and fifth request can begin. And, the fourth and fifth request are not both required to be finished, you only need one to finish, whichever one is first. Also imagine, for each request you have to create an error handling situation.
I hope you can see that using callbacks alone, to complete the task just mentioned, using something like the XMLHttpRequest
web
API will either lead to a pyramid of callback functions:
// A function within a function within a function within a function etc... getData1(function(x){ ... getMoreData2(x, function(y){ ... getMoreData3(y, function(z){ // on and on it could go }); }); })(); // oh no the pyramid of doom!
Or, a long list of linked callback functions:
// function 1 calls function 2, function 2 calls function 3 etc... getData1(function(x){ ... getMoreData2(x) })(); getData2(function(y){ ... getMoreData3(y) }); getData3(function(z){ // on and on it could go });
Either way, many consider this situation, callback hell.
To combated the complexity of complicated asynchronous programming and avoid callback hell ES2015 provided the native Promise API as an alternative to callback functions alone. The remainder of this chapter will cover the new Promise API in further detail.
Notes:
- Originally, promises were called "Futures" and were a part of the DOM spec. Thankfully, in the end it ended up in the ECMAScript specification so all JavaScript runtimes could use Promises for asynchronous programming.
- Unfortunately, IE does not support promises and it will have to be polyfilled if you need IE support. However the Edge browser (12+) does have support.
7.3 : Producing & Consuming a Promise
An instance of a Promise
is basically a function that typically houses asynchronous routines with two special functions to be called once the asynchronous work either completes or fails (i.e. resolve()
or reject()
). In short, a Promise provides the boilerplate around asynchronous code which results in an interface/methods for working with asynchronous code. Consider the methods and static methods provided by Promises:
-
Promise.all()
-
Promise.prototype.then()
-
Promise.prototype.catch()
-
Promise.prototype.finally()
( ES2018 ) -
Promise.race()
-
Promise.reject()
-
Promise.resolve()
These methods and static methods provide a cleaner API when dealing with complicated relationships among asynchronous code.
To produce a promise one only need to construct an instance of a promise from the Promise
constructor passing the constructor one argument known as the executor function. The executor function is passed two argument functions, the first is called resolve
, and the second argument is called reject
.
// Create a Promise called myPromise const myPromise = new Promise( (resolve, reject) => { // this is an executor function try{ // do some async work, like an XHR request or a setTimeout() // ... // then call resolve() when done, pass it some data setTimeout(function(){resolve('foo');}, 1000); // comment out the line above and un-comment line below to see error thrown // foo; }catch(error){ // if an error occurred, call reject() reject(error); } } ); // Consume myPromise myPromise.then( // takes two functions, // if promise calls resolve() this first function is called (data) => { console.log(data); }, // logs 'foo' // if promise calls reject() this second function is called (error) => { console.log(error.toString()) } // logs Error );
Let's take the promise API for a spin. In the code example below I am wrapping a Promise
around the older XMLHttpRequest
object and concealing its older callback function API in order to create a mini Github Promise based API.
// Create a function that will return a Promise const getGitHubData = (gitHubRESTPath) => { // return a Promise so the .then() method can be called on returned value return new Promise((resolve, reject) => { // this is the executer function // start asynchronous work const xhr = new XMLHttpRequest(); xhr.onload = function () { // callback function if (this.status === 200) { // call resolve() with async results when async work is done resolve(this.response); } else { // call reject() with statusText if server returns anything but a 200 reject(this.statusText); } }; xhr.onerror = function () { // callback function // call reject() with error message if XHR errors occur reject('XHR Error:'); }; xhr.open('GET', 'https://api.github.com/' + gitHubRESTPath); xhr.send(); }); }; // Now call getGitHubData() and consumer the Promise wrapped around XMLHttpRequest // Get number of stars for React on github getGitHubData('repos/facebook/react').then( // use then() to consume the eventual results //resolve function (response) => { // convert the JSON string response to a JS object console.log(JSON.parse(response).stargazers_count); // logs 11XXXX }, //reject function (error) => { console.log(error); } ); // Send a bad URL, to see reject function run getGitHubData('repos/nothing/nothing').then( // use then() to consume the eventual results //resolve function (response) => { // convert the JSON string response to a JS object console.log(JSON.parse(response).stargazers_count); }, //reject function (error) => { console.log(error); // logs 'Not Found' } );
By wrapping the Promise
itself with the getGitHubData
function I can call the getGitHubData
function like an API. The "mini api" provides a cleaner interface for working with asynchronous HTTP requests to the Github REST API because a Promise is used/returned.
To consume the returned promise the then()
method is used to capture both the resolution of the asynchronous code and potentially any resulting errors (i.e. then()
takes two arguments. First argument is resolution function, called when promise is resolved, second is a rejection function, called if the promise is rejected).
Notes:
Promise()
7.4 : Consuming Promises
Since promises are native to the language several JavaScript runtimes can make use of Promises. For example, the new Fetch API
, which replaces the older XMLHttpRequest
API returns a promise by default. Thus, instead of having to create a promise, all you have to do is consume the promise returned from calling fetch()
.
I've re-written the code from the previous section to use the new Fetch API
. By using the Fetch API
I don't need to produce a Promise
object manually, one is simply given to me by the web platforms Fetch API
. As you can see in the code example below when a promise is returned all that is left to do is to consume the promise using promise methods (e.g. then()
method)
// Get number of stars for React on github fetch('https://api.github.com/repos/facebook/react') .then( // use then() to consume the eventual results //resolve function (response) => { // note that calling response.json() is itself a promise response.json().then(json => console.log(json.stargazers_count)); // logs 11XXXX }, //reject function (error) => { console.log(error.toString()); // logs "undefined" } ); // Send a bad URL, to see reject function run fetch('htttps://www.badnogoodurl.com') .then( // use then() to consume the eventual results //resolve function (response) => { // note that calling response.json() is itself a promise response.json().then(json => console.log(json.stargazers_count)); // logs 11XXXX }, //reject function (error) => { console.log(error.toString()); // log "undefined" } );
7.5 : Producing an already Resolved or Rejected Promise
The Promise
API offers the Promise.resolve()
and Promise.reject()
static methods that will produce an instance of a Promise
that has either been resolved or rejected with a given value. This can be handy when needing to quickly create a promise in a specific state without dealing with an executor function.
The Promise.resolve()
static method returns a resolved promise with a specific value passed to it:
Promise.resolve('value1') .then(value => console.log(value)); // logs "value1" // The above is a shortcut for this: new Promise(resolve => resolve('value2')) .then(value => console.log(value)); // logs "value2"
The Promise.reject()
static method returns a rejected promise with a specific value passed to it:
Promise.reject('error1') .then(() => {}, error => console.log(error)); // logs "error1" // The above is a shortcut for this: new Promise((resolve, reject) => {reject('error2');}) .then(() => {}, error => console.log(error)); //logs "error2"
Don't over think these two static methods they simply side step the executor function and return a promise in a specific state.
Notes:
-
The
Promise.resolve()
static method is typically used to wrap a Promise around a value.
7.6 : Chaining Promises with then()
The then()
method is used to provide a resolution and rejection function for a promise. Remember, a promise runs/contains asynchronous code. This asynchronous code when complete can call one of two functions either resolve()
or reject()
. The then()
method accounts for both of these calls. If resolve()
is called then the first function to then()
is invoked. If reject()
is called then the second function pass to then()
is called.
In the code example below I am using then()
to extract the commits on Github for React, then extract the author of the last commit, then log that author to the console.
// get the name of the last person to commit to the React github repository // fetch all commits for React fetch('https://api.github.com/repos/facebook/react/commits') // handle response, pass json to next then() .then(response => response.json(), error => console.log(error)) // fetch user from Github API .then((json) => { // use commit data to get user name of last person to commit // use user name to get author data // pass response to next then() return fetch('https://api.github.com/users/'+json[0].author.login); }, error => console.log(error)) // handle response pass json to next then() .then(response => response.json(), error => console.log(error)) // get name, pass it to the next then(), note I am not passing a promise but just a simple value .then(user => user.name, error => console.log(error)) // log the name of the last person to commit code to the react repository .then(name => console.log(name + ' is the last person to commit to React repository'));
It is important to remember, and you may have noticed in the previous code example, that the then()
method can be used to chain any value not just promises. In the code example below I demonstrate using the promise API to simply transfer data through a promise chain.
// create a promise that is already resolved with a specific 'foo' value. var myPromise = Promise.resolve('foo'); // show how a promise can chain any value indefinitely. myPromise.then((data)=>{ return data; }).then((data)=>{ return data; }).then((data)=>{ console.log(data); // logs 'foo' });
However, the then()
method was specifically designed to chain promises that are doing asynchronous activities. In the following code example I use the promise chain to verify all the starwars API endpoints are functioning.
let starWarsAPICheck = []; console.log('Wait for it...! Data from another Galaxy yo!'); // chain a set of promise to occur one after the other // check the starwars api to see if all endpoints are working. fetch('https://swapi.co/api/people') .then((response)=>{ starWarsAPICheck.push(response.ok); return fetch('https://swapi.co/api/planets'); }).then((response)=>{ starWarsAPICheck.push(response.ok); return fetch('https://swapi.co/api/films'); }).then((response)=>{ starWarsAPICheck.push(response.ok); return fetch('https://swapi.co/api/species'); }).then((response)=>{ starWarsAPICheck.push(response.ok); return fetch('https://swapi.co/api/vehicles'); }).then((response)=>{ starWarsAPICheck.push(response.ok); return fetch('https://swapi.co/api/starships'); }).then(()=>{ console.log(starWarsAPICheck); // logs [true, true, true, true, true, true] }); // Note the previous chain all happen sequentially // To make all the calls run in parallel, Promise.all() could be used.
One should note that any error returned within the first function passed to then()
will result in the next then()
running the rejection function (i.e. the second function padded to then()
).
// create a promise that is already resolved with a specific a 'foo' value. var myPromise = Promise.resolve('foo'); // show how error returned in the second then(), calls reject callback function, but chain goes on myPromise.then((data)=>{ return data; }).then((data)=>{ throw Error('yo this then() is no good'); // will cause reject callback in next then() to run }).then( (data)=>{console.log(data)}, // does not run (error)=>{console.log(error.toString())}, //logs 'yo this then() is no good' ).then((data)=>{ console.log('note this still runs'); console.log(data); // logs undefined, foo is undefined });
Basically, using then()
allows a developer to push data through a set of functions that can deal with both synchronous and asynchronous results interchangeable. When an error occurs, the chain does not stop, the next then()
simply captures the error and runs the second function passed to it.
Notes:
-
If you don't provide
then()
a resolve function or provide a non-function, no error will occur. The nextthen()
resolve function is passed anundefined
value.
7.7 : Catching Rejections in the Chain with catch()
When rejection errors occur within a promise chain the catch()
method can be used to capture the rejection. This can be handy for two reasons. First off, you can write a single error handle for a long list of chained promises and the first rejection in the chain will get passed on to the catch()
. Secondly, because catch()
returns a promise, just like then()
, you can keep chaining with more then()
's or catch()
's. Basically, one could perform several asynchronous routines and use one catch()
if any errors occur, then continue on with more then()
's or catch()
's' at will.
In the code example below a broken API call is kicked off and a catch()
method at the end of the chain captures the error.
fetch('https://api.github.com/repos/fanebook/react/commits') // broken URL, facebook not fanebook .then(response => response.json()) .then((json) => { // this throws an error because the json passed in is bad // it is bad because the original URL is wrong facebook, not fanebook // so json[0].author.login is a bad reference // so this function throws an error which is caught in the first catch() in the chain return fetch('https://api.github.com/users/'+json[0].author.login); }) .then(response => response.json()) // response.json() returns a promise .then(data => console.log(data.name + ' is the last person to commit to React repository')) // never logs .catch(error => { // any error issue in the previous then()'s are trapped here console.log(error.toString()); }) .then(() => { console.log('yup you can still chain after a catch()'); });
Notes:
-
The
catch()
method is typically used over providing eachthen()
with a rejection parameter. -
Notice how readable the code in this section becomes once a single rejection function was used via a
catch()
over providing a rejection function for eachthen()
. -
The
catch()
method is simply a short-hand for,.then(null, error => { // do something with error })
.
7.8 : Running a Final Function, Regardless of Promise Fulfillment State with finally()
When a situation exists that you'd like to run a function regardless of if a promise is resolved or rejected you can use the finally()
method. This method makes it possible to run code no matter what occurs previously in the promise chain.
In the code example below I demonstrate how the finally()
method is invoked regardless of the fulfillment paths through the promise chain.
let stateOfLoading = 'Not Loading'; // this is the default state fetch('https://swapi.co/api/peoples/') // breaks because the URL is people not peoples .then((response) => { if(!response.ok){ // run if fetch fails throw Error('api call broke'); // catch() is called } stateOfLoading = 'Loaded'; return stateOfLoading; // passed from finally() to the last then() in chain }) .catch(error => { stateOfLoading = 'Loading Error'; return stateOfLoading; // passed from finally() to the last then() in chain }) .finally(() => { // finally does not take in parameters // runs no matter what, because we want to return state to default // regardless of if the catch or then is fulfilled stateOfLoading = 'Not Loading'; }) /* Basically a finally() eliminates redundant functions in a then() The above finally is just shorthand for using a then() like so: .then( () => { // finally does not take in parameters // runs no matter what stateOfLoading = 'Not Loading'; }, () => { // finally does not take in parameters // runs no matter what stateOfLoading = 'Not Loading'; }, ) */ .then((data) => { // get value from previous catch() or then() // change the initial api call from /people to /peoples to get different values console.log(data); // either 'loaded' or 'loading error' console.log(stateOfLoading); // verify finally statement returned state to default });
Notes:
-
The
finally()
method was added in ES2018 . - This method is typically used to clean up things that might have been changed while a chain of Promises are being resolved (i.e. turn a loader UI off and return either data or an error in place of the loading UI).
7.9 : Waiting for a List of Parallel Promises to Complete with Promise.all()
If you want to kick off several asynchronous activities simultaneously using promises and get notified when they are all complete the Promise API offers Promise.all()
. The Promise.all()
static method takes an array of promises and will return a single promise when all the promises in the Array are fulfilled.
Promise.all([ fetch('https://api.github.com/repos/facebook/react/commits'), fetch('https://api.github.com/repos/facebook/react/stargazers') ]) .then(([commits, stargazers]) => { console.log(commits.ok, stargazers.ok); // logs true, true });
7.10 : Waiting for the First, of a List, of Parallel Promises to Complete with Promise.race()
If you want to kick off several asynchronous activities simultaneously using promises and get notified when the first promise fulfills the Promise API offers Promise.race()
. The Promise.race()
static method takes an Array of promises and will return only the first promise that is fulfilled.
Promise.race([ fetch('https://api.github.com/repos/facebook/react/commits'), fetch('https://api.github.com/repos/facebook/react/stargazers'), ]) .then(response => { console.log(response.url); // logs whichever one finishes first });
Chapter 8 : ES2017 Async Functions & await operator
This chapter covers asynchronous functions (i.e (async () => {})();
) and the use of the await
operator with async
functions so that asynchronous code can be written to look more like synchronous/procedural code.
8.1 : The async
/ await
syntax is not a replacement for promises
It would be a mistake thinking async
functions and the await
operator completely replaces the use of promises. Or, that one need not clearly understand promises before using async
functions and the await
operator. Thus, an in-depth reading of the previous chapter is necessary to comprehend this chapter.
An async
function returns a promise that is typically resolved using the await
operator. Think of an async
function as a syntactical replacement for an executor function and the await
operator as an inline syntactical replacement for then()
.
Ultimately what you need to keep in mind is that the async/await syntax enhances promises, it does not replace promises!
8.2 : The benefits of using async
functions and the await
operator
Using async
functions and the await
operator to handle asynchronous code is a stylistic choice with some subjective benefits over traditional promises.
In the code example below I've augmented the Github promise code from the previous chapter creating a utility function that uses an async
function and the await
operator instead of a long .then()
promise chain.
let stateOfLoading = 'Not Loading'; // this is the default state // add async keyword to arrow function, now this function returns a promise and can use await operator const getLastGitHubCommitAuthor = async (owner, repo) => { try { stateOfLoading = "Loading"; // await pauses this function until the promise from fetch is fulfilled const commits = await fetch(`https://api.github.com/repos/${owner}/${repo}/commits`); // await pauses this function until the promise from .json() is fulfilled const jsonCommits = await commits.json(); // json() returns a promise // await pauses this function until the promise from fetch is fulfilled const author = await fetch(`https://api.github.com/users/${jsonCommits[0].author.login}`); // await pauses this function until the promise from .json() is fulfilled const jsonAuthor = await author.json(); // json() returns a promise return jsonAuthor.name; // a string is returned, but it is wrapped in a Promise } catch (error) { // any and all errors from the try and caught here, sort of like .catch() stateOfLoading = 'Error Loading'; throw new Error(error); // throw error so async function will return rejected promise. } finally { // can replace the .finally() method functional from a promise chain stateOfLoading = 'Not Loading'; } }; // Now consume the async function, which returns a promise, with promise methods. getLastGitHubCommitAuthor('facebook', 'react') .then(name => console.log(name + ' is the last person to commit to the React repository')) .catch(error => console.log(error.toString()));
Now compare the async/await solution you just looked at to the promise only solution:
let stateOfLoading = 'Not Loading'; // this is the default state const getLastGitHubCommitAuthor = (owner, repo) => { // explicitly return a promise from the end of the chain return fetch(`https://api.github.com/repos/${owner}/${repo}/commits`) .then(response => response.json()) // json() returns a promise .then(jsonCommits => fetch(`https://api.github.com/users/${jsonCommits[0].author.login}`)) .then(response => response.json()) // json() returns a promise .then(jsonAuthor => jsonAuthor.name ) .catch((error) => { stateOfLoading = 'Error Loading'; throw new Error(error); }) .finally(() => { stateOfLoading = 'Not Loading'; }); }; // Now consume the async function, which returns a promise, with promise methods. getLastGitHubCommitAuthor('facebook', 'react') .then(name => console.log(name + ' is the last person to commit to the React repository')) .catch(error => console.log(error.toString()));
You should note the async/await solution offers the following subjective benefits:
- Notice how asynchronous code has moved closer to a synchronous/procedural style (i.e. do this, then do this, then do this). Many people find this easier to read and maintain than using method chaining boilerplate.
-
Notice the reduction of chaining boilerplate (i.e. only used one
then()
not a long chain of them). -
Notice the efficiency of using a
try
,catch
, andfinally
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 tothen()
means less cruft around errors being thrown. -
Consider also that using the async/wait syntax could flat out potential child chains (i.e.
then()
chains inside ofthen()
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:
-
The async keyword works with all functions; function expressions (e.g.
const foo = async function () {};
), function declarations(e.g. async function foo() {}
), method definition (e.g.let obj = { async foo() {} }
), and the arrow function (e.g.const foo = async () => {}
). -
Don't forget, the
await
keyword only works inside aasync
function and is used to pause a function until a promise is fulfilled. -
The async keyword placed in front of a function creates a special kind of function object called, "AsyncFunction". The AsyncFunction constructor is not a global object but you can obtain a reference to the constructor using
Object.getPrototypeOf(async function(){}).constructor
.
8.3 : Asynchronous functions return an implicit Promise
If a promise is not explicitly returned from an async function then one is implicitly returned. The result is that a promise is always returned from an async
function.
// an async function that returns the value 5 var myAsyncFunction = async () => { return 5; } // verify async returns an implicit promise, wrapping the value 5 console.log(myAsyncFunction() instanceof Promise); // logs true // use then() to get value returned from myAsyncFunction myAsyncFunction().then((value) => { console.log(value) }); // logs 5
8.4 : The await
operator awaits a Promise or creates an implicit Promise
Any value await
'ed for that is not a Promise
is implicitly converted to a resolved promise. (e.g. Promise.resolve(value)
).
// an async function that returns the value 5 var myAsyncFunction = async () => { const awaitedPromiseValue = await 5; // The above is like explicitly writing: // const awaitedPromiseValue = await Promise.resolve(5); } // use then() to get value returned from myAsyncFunction myAsyncFunction().then((value) => { console.log(value) }); // logs undefined // nothing was returned so implicitly undefined is returned // i.e. Promise.resolve(undefined);
Notes:
-
If a promise that is being
await
'ed for is rejected the await expression throws the rejected value.
8.5 : The await
operator is sequential
It might be obvious but just like the then()
method the await
operator waits for promise resolution/fulfillment before moving on to the next line of code so to speak. This means to run parallel asynchronous routines instead of sequential routines the static Promise
methods Promise.all()
and Promise.race()
can be used.
(async () => { // create async function that is immediately invoked // sequential fetch's const responsePlants = await fetch('https://swapi.co/api/planets'); // API call above has to finish before the API below can start const responseFilms = await fetch('https://swapi.co/api/films'); console.log(responsePlants.ok, responseFilms.ok) // logs true true // parallel fetch's, faster than sequential fetch's using Promise.all() const [parallelResponsePlanets, parallelResponseFilms] = await Promise.all([ fetch('https://swapi.co/api/planets'), fetch('https://swapi.co/api/films') ]); console.log(parallelResponsePlanets.ok, parallelResponseFilms.ok) // logs true true // parallel fetch's, return first one to finish, using Promise.race() const fastestResponseFromStarWarsApi = await Promise.race([ fetch('https://swapi.co/api/species'), fetch('https://swapi.co/api/vehicles'), fetch('https://swapi.co/api/starships') ]); console.log(fastestResponseFromStarWarsApi.ok); // logs true })()
Chapter 9 : Using ES2015 Modules Today
This chapter will focus on defining the need for modules in general, the difference between module syntax and the module loading API, and then briefly explore the browsers module loading API.
9.1 : Why JavaScript Modules (aka EcmaScript Modules)?
Before ES2015 JavaScript didn't offer native modules. Oh, everyone faked it for years in all sorts of amazing ways but all of these solutions were temporary fixes to a significant deficit with the language. What was needed was a native module syntax built into the language and a runtime loading system that offered the following:
import export
All of the above requirements as of today have been satisfied by:
-
The JavaScript module syntax (e.g.
import
andexport
) -
Runtime module loading API's. As an example, the web platforms Javascript module loading API
that
loads module files (i.e.
<script type="module" src="myModule.js" />
)
Notes:
- JavaScript modules are based on CommonJS modules (aka Node.js modules) . In the future, JavaScript modules will likely replace commonJS modules. In fact, JavaScript module syntax was introduced experimentally into Node.js v8.5.0. But keep in mind imported values in CommonJS are copies of values while imported values in ES modules are live read-only values.
9.2 : Understanding Module Syntax V.S. the Module Loader API
JavaScript modules require two separate parts/specifications working together. The first part
specifies how the module is defined and the implications of this definition within the language.
(e.g. import
and export
). The second part is how the module is loaded
(e.g. The browser module loader API e.g. <script type="module" src="myModule.js" />
and the Node ES module Loading API
).
The module syntax is what is defined by the ECMAScript standard . The loader API is not defined by the ECMAScript standard. The details on how a module is loaded are details that JavaScript runtimes have to iron out (e.g. Browsers and Node.js).
Notes:
- Still, as of 2019, it is common to see module bundlers (e.g. Webpack and Parcel) used in place of a native module loading API (e.g. developers use bundlers today to transform ES module syntax into historical ES5 script files and these files don't use the native web module loading API).
9.3 : Loading/Running Static Modules in a Browser using <script type="module">
Before examining how static modules are loaded by browsers lets recap how the <script>
element was used historically to load/run non-ES2015
JavaScript
modules (aka script files, classic script files, or historical script files instead of ES
modules).
Below three external <script src=""></script>
's' and one inline <script></script>
are run in the the HTML document:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> <script src="script1.js"></script> <script src="script2.js"></script> <script src="script3.js"></script> <script>console.log(this) // logs window object</script> </body> </html>
Note the following about the above loading/running of the script1.js
, script2.js
,
and script3.js
files:
- Values define in script files loaded into an HTML document are defined in the global scope (except for function and block scoped values contained in the script files).
-
By default, the
script1.js
is loaded/parsed by the browser engine before moving on toscript2.js
(unless theasync
attribute is used). By default, scripts are loaded/parsed synchronously. -
By default all
<script>
's' block the HTML parser (unless thedefer
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:
-
The
type="module"
attribute is required so the browser will use the native browser module loading API to parse/load each module. -
Each
<script>
has its own entry file and dependency graph . -
An ES module can be written inline or be written in a separate file and pulled into the current
HTML
file using the
src
attribute. - An ES module runs code in strict mode by default.
-
Values defined in an ES module that are not
export
'ed are scoped only to that module, these values don't leak into the global scope. -
ES modules are singletons in the sense that only a single instance of it
exists. Basically, only one instance of it is used no
matter how many times it is
import
'ed. -
ES Modules still have access to the global context (i.e. the top level runtime scope). In a
browser this would be the global
window
context. However, the use of the keywordthis
will not work inside of a module to reference the global runtime scope. -
The use of the
import
andexport
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). - Exports from an ES module are static. Meaning, once they have been defined and imported they can not be changed later.
- ES Modules and their dependencies (i.e. imported modules) are fetched with CORS .
-
ES Module
<script type="module">
's' usedefer
by default.
Notes:
-
The following web browsers all support loading modules using the
<script type="module">
: Edge 17+, Firefox 61+, Chrome 63+, Safair 11.1+, iOS Safari 11.2+, Chrome Android 69+ . -
Paths to modules can be full URLs, or relative URLs starting with
/
,./
, or../
. As of today, what won't work unless using an asset bundler, is specifying a module by name of module package (i.e.import {React} from 'react'
). In other words, if you NPM install React you will have to import that module using a relative URL not the name of the package installed in node_modules. -
The file extension
.js
can be omitted from the URL specifier string. - ES modules are static meaning they can't be changed at runtime. This allows things like static checking and optimizations when importing and bundling.
-
You can request a module hosted on another domain if it uses CORS
(e.g.
import value from 'https://otherdomain.com/modules/module1.js';
). - Using the native web browser module loading API for systems that have more than 100+ modules could come at a performance cost (depends upon the size of dependency graph) and thus asset bundlers are in common use today to avoid performance related issues in production.
-
All static module dependencies have to be downloaded and executed before the code will run.
If you are wondering how you conditionally import a module you will have to use a dynamic
import (i.e.
import('module1.js')
). -
Inside a module the statement
import.meta
will return an object containing aurl
property housing the location the module was imported from (e.g.console.log(import.meta); //logs { url: "file:///home/user/module1.js" }
).
9.4 : Conditionally loading a Module using import()
The
import
statement has a dual purposed. It can be used to import other modules statically as well as
dynamically. When the import statement is used as
a function, and the module path/specifier is passed to it as an argument, a module can be loaded
dynamically (e.g. import('./module1.js')
)
). The return value from using import
as a function is a Promise
.
Essentially
this means you can
asynchronously load ES modules after all the static modules have been loaded into the runtime.
In the code below after clicking anywhere on the HTML document, in the browser window, the lodash forEach
ES module
is loaded from the unpkg.com
CDN and used to log out the context of an array.
<html> <head> <meta charset="UTF-8" /> <style> body { cursor:pointer; } p { position: relative; height: 100%; text-align: center } strong { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-weight: normal; } </style> </head> <body> <p> <strong> Click anywhere in this window <br> to load a module dynamically.<br> Then view console. </strong> </p> <script type="module"> const getModule = () => { (async () => { // I am in an inline module, using import('module') const module = await import('https://unpkg.com/[email protected]/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:
- The following web browsers all support dynamic imports: Chrome 63+, Safari 11.1+, iOS Safari 11.2+, Chrome Android 69+ .
-
Paths to dynamic modules can be full URLs, or relative URLs starting with
/
,./
, or../
.
9.5 : Parsing JS Module Syntax with Asset Bundlers
Asset bundlers like Webpack and Parcel are in wide use today. These asset bundlers are used because they provide a system that will modularize and load many different asset types and formats. Not just JavaScript Modules! For example, Webpack and Parcel can treat things like JavaScript, HTML, CSS, and Image files like modules, that can be imported into each other, and then bundle these differing assets/formats into a production state.
Bundlers like Webpack and Parcel can also analyze JavaScript files written using ES module syntax and create from these files a dependency graph that is then used to output one or many (aka code splitting) production runnable ES5 files. The outputted JavaScript won't typically use the web browser module loading API or module syntax but instead will load as an ES5 script file would have before ES modules.
Basically asset bundlers when dealing with ES modules can read ES2015 module syntax and
convert it to runnable bundled ES5 code in the form of a historical script file. Then these files
are loaded into a
browser using the
historical <script>
element.
Today, most developers use an asset bundler. The reason most developers choose a bundler over the browser loading API is a combination of performance concerns, browser support concerns, and the fact that bundlers will bundle not only JavaScript files, but also other collaborating assets (i.e. treating HTML, CSS, and image files like importable modules ).
Notes:
- ES module syntax and semantics are lost when bundled by modern day assert bundlers.
- Asset bundlers can do much more than simply convert ES6 module syntax into runnable ES5 code. Fundamental to modern day bundlers is the process of taking ES modules syntax and JavaScript 2015+ code (via Babel) and converting it to script files that can be run in ES5 environments.
-
Asset bundlers don't yet leverage the JavaScript module API found in modern web browsers
(i.e.
<script type="module" />
). -
Using a package name to load a module won't work when using the browser module loading API.
However, when using an asset bundler it is possible to use a packages name because the
bundler can be configured to correctly find the path to the module identified by a
name (e.g.
import _ from 'lodash';
). - Rollup.js is a bundler just for JavaScript modules.
- Asset bundlers will typically accept CommonJS syntax or ES module syntax .
Chapter 10 : Writing ES2015 Module Syntax
This chapter will focus on ES modules syntax itself. ES Module syntax is commonly used by asset
bundlers or more recently by the web browsers <script type="module">
module loading API.
10.1 : Module Syntax Overview
A lot of details and stylistic choices exist around the importing and exporting of values in JavaScript modules. However, fundamentally JS module syntax basically boils down to these two concepts:
-
A module can export multiple values and or a single default value from within a
module
using
the keywords
export
anddefault
:
// 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';A module can simultaneously import multiple values and a single default value from other modules using the keywords
import
and from
along with a URL
specifier string identifier (i.e. the relative or full URL file path to the module whose values are being
imported):
// 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:
-
An URL specifier must be a path to a module using full URLs, or relative URLs starting with
/
,./
, or../
. However, if one is using an asset bundler (e.g. webpack) the URL specifier can also be the name of the module package (i.e.import {React} from 'react'
) if the asset bundler has been configured to sort out the path to the module by package name. -
The file extension
.js
can be omitted from the URL specifier string. - Imports are hoisted to the top of the scope regardless of where they appear in the module. Thus, most people place all imports at the start of a module.
- Where exporting happens (i.e. inline or end of the module) is subjective preference based on exporting styles.
-
Imports and exports can't natively be conditionally imported or exported without the use of a
dynamic
import()
(i.e. don't think you can place imports inconditional
statements without using a dynamicimport()
). -
ES modules are in
strict mode
by default. -
ES modules have access to the global scope (e.g.
window
) but not usingthis
value. The global scope has to be reference with a point to the global scope (e.g.window
). - Circular dependencies are supported (e.g. module 1 can import a value from modules 2, and at the same time module 2 can import value from module 1.). These modules depended upon each other and because modules are static this circular dependency is resolved before either module is executed.
10.2 : Importing and Exporting Named values
The export
keyword can be used to export an unlimited number of named values from a
module.
Three styles are available when exporting named values from a module:
1. Exporting values inline when expressed/declared:
// module1.js // exporting values inline, when defining export const simpleString = 'simpleString'; export const simpleFunction = () => {}; export const simpleNumber = 5; export class myClass {}; export function myFunction(){}; // Note these won't work // this will throw an error an expression or declaration is expected export 5; export simpleString;
2. Exporting values by reference:
// module1.js const simpleString = 'simpleString'; const simpleFunction = () => {}; const simpleNumber = 5; class myClass {}; function myFunction(){}; // style for multiple references export {simpleString, simpleFunction, simpleNumber, myClass, myFunction};
3. Exporting renamed referenced values:
// module1.js const sF = 'simpleString'; const sS = () => {}; const sN = 5; class mC {}; function mF(){}; // exporting multiple renamed references export {sF as simpleFunction, sS as simpleString, sN as simpleNumber, mC as myClass, mF as myFunction}
Four styles are available when importing the above named exported values into another module:
1. Importing a single named export:
// This is module2.js, import a single value into this module from module1.js import {simpleString} from './module1.js'; console.log(simpleString);
2. Importing multiple named exports:
// This is module2.js, import multiple values into this module from module1.js import {simpleString, simpleFunction, simpleNumber, myFunction} from './module1.js'; console.log(simpleString, simpleFunction, simpleNumber, myFunction);
3. Importing multiple named exports using an import namespace (i.e. importing an entire module):
// This is module2.js, import all values into this module from module1.js import * as m1 from './module1.js'; console.log(m1.simpleString, m1.simpleFunction, m1.simpleNumber, m1.myClass, m1.myFunction);
4. Importing multiple named exports, renamed:
// This is module2.js, import renamed values into this module from module1.js import {simpleString as sS, myClass as mF } from './module1.js'; console.log(sS, mF);
10.3 : Importing and Exporting a Default Value
A module can export one default value using the default
keyword in combination
with the export
keyword.
Two styles are available when exporting a default
value from a module:
1. Inline default exporting:
// moduleA.js // Of course only one default can be used per module. // Each export below would not live in the same file, it would throw an error // Consider each line below to be in its own module // literal values (no const or let used) export default 'string'; export default 5; export default true; export default {}; export default []; // expressions const simpleString = 'simpleString'; const simpleNumber = 5; export default simpleString; export default simpleNumber; export default (() => {}); // declarations export default class myClass {} // semicolon optional with this style export default function myFunction(){} // semicolon optional with this style // unnamed declarations export default class {} // semicolon optional with this style export default function(){} // semicolon optional with this style
2. Renamed to default exporting
// moduleA.js const simpleString = 'simpleString'; // No default keyword here export {simpleString as default}; // Exporting as default
One style is available when importing the above default exports into another module:
1. Renamed default
// This is moduleB.js, import default from moduleA.js // assumes you know that moduleA.js exports one default value // the default is imported and renamed where it is imported import nameGivenHereToDefault from './moduleA.js'; console.log(nameGivenHereToDefault);
Notes:
-
In the end, default exports are just values, renamed to the word
default
. For this reason many avoid default exports and just use named exports . -
The word
default
, just like the wordnew
, can't be used as a variable name.
10.4 : Combining a Default and Named Exports When Exporting and Importing
Modules can export both a single default value, along with other named values:
// moduleA.js const myString = 'myString'; export default myString; const myFunction = () => { }; export const myNumber = 5; export { myFunction };
All the values export from moduleA.js above can be imported together using the following two styles:
By name:
// moduleB.js import nameGivenHereToDefault, {myFunction, myNumber} from './moduleA.js'; console.log(nameGivenHereToDefault, myFunction, myNumber);
Using a single namespace:
// moduleB.js import * as namespace from './moduleA.js'; console.log(namepsace.default, namespace.myFunction, namespace.myNumber);
10.5 : Imported Values Are Live References, Not Copies
ES modules do not create copied values when importing values. Instead, ES modules create pointers/references to live values.
In the code example below the counter
and addToCounter()
values are
imported into module1.js, from module2.js. From module1.js I call
the addToCounter()
function, imported from module2.js, which changes the value of counter
in module2.js and thus
module1.js also. This is because the values imported are live values not copies of values.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> <!-- Entry module is module1.js, dependency graph starts here --> <script type="module" src="./module1.js"></script> <!-- module1.js imports and uses module2.js --> <!-- module1.js import { counter, addToCounter } from "./module2.js"; console.log(counter); // logs 0 addToCounter(10); console.log(counter); // logs 10 // Note, can't do this: // counter = 10; --> <!-- module2.js export let counter = 0; export const addToCounter = function(amountToAdd) { counter = counter + amountToAdd; }; export const foo = "foo"; --> </body> </html>
Keep in mind that imported values can't directly be changed. In other words, in the code example
above directly changing the counter
value from module1 will throw an error.
Notes:
- In contrast to ES modules, CommonJS modules import copied values instead of live read-only values.
10.6 : Re-exporting imports
While not commonly done it's possible to re-export, imported values, using the URL string specifier. When re-exporting, the values never enter the scope of the module that is re-exporting them. Values simply pass through to the scope where they are not re-exported.
Re-exporting imported values can be done using the following styles:
Single namespace:
export * from 'myModule.js';
Named values:
export {value1FromMyModule, value2FromMyModule} from 'myModule.js';
Re-named values:
export {value1FromMyModule as renamed1, value2FromMyModule as renamed2} from 'myModule.js';
As a default value:
export {default} from 'myModule.js'; // or export {value1FromMyModule as default} from 'myModule.js';
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK