https://kdakan.github.io/ES6-Tutorial/
This is a tutorial on the ES6 (and some ES8) additions to javascript. You can run the examples directly on the Chrome developer console, without any transpiling.
- 1. Scoped variables
- 2. Destructuring
- 3. Default parameters
- 4. Rest parameters
- 5. Spread operator
- 6. Template literals
- 7. Classes
- 8. Arrow functions
- 9. Generators and iterators
- 10. Built-in objects
- 11. Object.assign() (mixins)
- 12. Object literal shortcuts
- 13. Proxies
- 14. ES6 modules
- 15. Promises
- 16. Async/await (ES8)
- 17. Fetch API examples
let n = 1;
defines a block scoped variable, where a block is enclosed by{
and}
, it is only visible inside the block, and it cannot be declared again inside the same block.let
can be used likewhere n is only visible inside the if block but not outsidelet someCondition = true; if (someCondition) { let n = 1; }
- In
es5
,var n = 1;
defines a function-scoped variable which is hoisted (its declaration is moved to top of the function declaring it, or becomes global if not declared inside a function), and var can define the same variable multiple times inside the same function but the last declaration will take effect, which can cause problems and it is not recommended to use var anymore const n = 1;
defines a block scoped variable, and its reference cannot be changed, meaning that it cannot be reassigned. However, aconst
object is not immutable, because its members can be reassigned, likeIf we need immutable objects,{ const o = {n: 1, s: "abc"}; o.n = 2; o.s = "xyz"; }
Object.freeze()
orimmutable.js
library can be used with more handy features
- Destructuring uses
[
and]
to assign multiple variables at once from an array, and{
and}
to assign multiple variables at once from object fields, likelet [x, y] = [3, 5]; //x=3, y=5 [x, y] = [y, x,]; //x any y are swapped, x value becomes y, y value becomes x let [, a, b, c] = [1, 3, 5]; //skips first element in the right-side array, a=3, b=5, c=undefined let {p, r, s} = {p: 3, r: 5, s: "abc"}; //p=3, r=5, s="abc" let {p:k, s:l} = {p: 3, r: 5, s: "abc"}; //k=3, r="abc", beware the weird ordering, that it is not k:p, but rather p:k let {m, n} = {m: 3, n: "abc"}; //m=3, n="abc", shorthand syntax when the variables and object fields have the same name let f = function(x, {y, z}, n) { //do something with x, y, z, n parameters } f(1, {y: 2, z: "abc"}); //call function f() with x=1, y=2, z="abc", n=undefined parameter values
- Parameters can be assigned default values, like
let f = function(x = 1) { //no need to use let y = x || 1; to give a default value of 1 if x is missing } f(); //missing parameter x=default value of 1, same as f(undefined); f(null); //parameter x=null, because it is not missing f(""); //parameter x="", because it is not missing let g = function(x = 1, {y = 2, z = 3}) { //do something with x, y, z parameters } g(7, {z: 9}); //missing parameter y=default value of 2
- Rest parameters, which is an array, replace the need for
arguments
(arguments
can also be used, but it is an object, not an array). Rest parameters use...someVariables
syntax, likelet sum = function(x, y, ...rest) { //here you can use rest parameter as an array } let total = sum("a", "b", 5, 7, 4, 67, 38); //parameter rest=[5, 7, 4, 67, 38]
- Spread operator
...someArray
, turns an array into a comma-separated expression, likelet f = function(x, y, z) { //do something with x, y, z parameters } f(...[3, 5, 7]); //call f() with parameters x=3, y=5, z=7 let a = [3, 5, 7]; let b = [20, 40, ...a, 60, 80]; //b=[20, 40, 3, 5, 7, 60, 80]
- Template literals are used for concatenating strings, like
let id = 123; let domain = "abc.com"; let url = `https://${domain}/item/${id}`; //url="https://abc.com/item/123"
- Classes support constructor, methods, and property getters and setters, like
is similar to the
class Employee { constructor(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } work(hours) { console.log(this.fullname + " is working for " + hours + " hours."); } get fullname() { //this is a property getter return this.firstname + " " + this.lastname; } set fullname(fullname) { //this is a property setter var names = fullname.split(" "); this.firstname = names[0]; this.lastname = names[1]; } } var emp = new Employee("John", "Doe"); emp.work(8); //prints "John Doe is working for 8 hours."
es5
versionfunction Employee(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } Employee.prototype = { work: function(hours) { console.log(this.getfullname() + " is working for " + hours + " hours."); }, getfullname() { return this.firstname + " " + this.lastname; }, setfullname(fullname) { var names = fullname.split(" "); this.firstname = names[0]; this.lastname = names[1]; } }; var emp = new Employee("John", "Doe"); emp.work(8); //prints "John Doe is working for 8 hours."
- A class can
extend
(inherit from) a super class, likeclass Person { constructor(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } } class Employee extends Person { constructor(title, firstname, lastname) { super(firstname, lastname); this.title = title; } } var emp = new Employee("Manager", "John", "Doe"); console.log(emp.title + " " + emp.firstname + " " + emp.lastname); //prints "Manager John Doe"
- Arrow functions are the preferred way to define callback functions and short functions, and offer a shorthand syntax, like
//add() and add2() do the same thing let add = (x, y) => x + y; let add2 = (x, y) => { return x + y; } let sum = add(1, 2); //sum=3 let sum2 = add(1, 2); //sum2=3 //single parameter let square = x => x * x; let sqr = square(2); //sqr=4 //parameterless let three = () => 3; let x = three(); //x=3 let arr = [1, 2, 3]; let total = 0; arr.forEach(x => total += x); //total=6 let doubles = arr.map(x => x * 2); //doubles=[2, 4, 6] let odds = arr.filter(x => x % 2 === 1); //odds=[1, 3]
- Arrow functions use lexical scoping, thus when used as a callback function,
this
from the outside is visible inside the arrow function, unlike the normal callback functions declared withfunction
keyword, likeclass MyClass { total = 0; sumWithArrowFunction = function() { this.total = 0; [1, 2, 3].forEach(x => this.total += x); } sumWithoutArrowFunction = function() { this.total = 0; let self = this; [1, 2, 3].forEach(function(x) { return self.total += x; //this.total does not work here, we need to first set it outside to a variable like self and use self inside }); } } let m = new MyClass(); m.sumWithArrowFunction(); console.log(m.total); //m.total=6 m.sumWithoutArrowFunction(); console.log(m.total); //m.total=6
- Generator functions create iterators, and iterables can be lazily iterated using for..of syntax, like
Here the
let numbers = function*(max) { for (let i=0; i<max; i++) { console.log("yield " + i); yield i; } } for (let n of numbers(3)) console.log("got " + n);
for..of
loop prints "yield 0", "got 0", "yield "1, "got 1", "yield 2", "got 2", so the numbers are iterated lazily. This can be valuable when the iterator is doing expensive work like an expensive calculation, going to the database, or using network operations
-
There are new built-in objects and objects with new additional methods, like
Number
,Array
,Set
(hashset),Map
(hashmap/hashtable/dictionary),WeakSet
,WeakMap
.WeakSet
andWeakMap
do not hold strong pointers to their items, so that the item can be garbage collected, and they cannot be iterated. UsingWeakMap
andWeakSet
instead ofMap
andSet
can prevent memory leaks. -
Some examples of new
Array
methods:let array = [1, 5, 10, 20]; let a = array.find(item => item > 3); //returns first match, 5 let ind = array.findIndex(item => item > 3); //returns index of first match, 1 array.fill('a'); //fills array with 'a', array becomes ['a', 'a', 'a, 'a'] array.fill('x', 2, 3) // fills with 'x' starting from index 2 to 3 (excluding 3), array becomes ['a', 'a', 'x, 'a'] let array2 = new Array(3); //creates array of length 3 with empty (undefined) items let array3 = Array.of(1, 2, 3); //array3=[1, 2, 3] let array4 = Array.from(document.querySelectorAll('div')); //creates araay from a non-array DOM object collection
Object.assign(o1, o2)
merges members of o2 onto o1 (o2 is also called mixin),Object.assign()
can have more than 2 parameters, and will merge all into the first parameter object
- Object literal shortcut syntax can be used, like
let model = "Porche"; let year = 2018; //both create an object with members model="Porche", year=2018 and run() let car = { model: model, year: year, run: function() {console.log("running");} } let car2 = { model, year, run() {console.log("running");} } let fieldname = "model"; let fieldvalue = "Ferrari"; //both create an object with member model="Ferrari" let car3 = {}; car3[fieldname] = fieldvalue; let car4 = { [fieldname] : fieldvalue }
Proxy
is a wrapper around an object, and lets us intercept getting and setting properties, and also intercept calling methods on the wrapped (proxied) object.- We can use a
Proxy
for getters and setters, likelet horse = { color: 'white', hasTail: true } let proxyHorse = new Proxy(horse, { get: function(target, property) { if(property === 'color') return 'Brilliant ' + target[property]; else return target[property]; }, set: function(target, property, value) { if (property === 'hasTail' && value === false) console.log('You cannot set it to false!'); else target[property] = value; }, apply: function(target, context, args) { if(context !== unicorn) return "Only unicorn can use hornAttack"; else return target.apply(context, args); } }); console.log(proxyHorse.color) //prints "Brilliant white" proxyHorse.hasTail = false; //does not set hasTail to false, instead prints "You cannot set it to false!"
- We can also intercept calls to
apply
,delete
,define
,freeze
,in
,has
, etc.
- Before ES6, there were several module systems and libraries to support them, like
AMD
modules andCommonJS
modules. ES6 module syntax is similar to theCommonJS
syntax. - A module can be declared by using
export
and accessed from another module by usingimport
. There are two kinds of exports, named export and default export. A module can "named export" from nothing to multiple things and "default export" either nothing or one thing, likeWe should use//inside lib.js export function add(x, y) { return x + y; } export const multiply = (x, y) => { x * y } export class ComplexNumber { ... } export default const PI = 3.1459...; //inside app.js import * as mathlib from './lib.js' let sum = mathlib.add(1, 2); import piNumber, { add, ComplexNumber } from './lib.js' let sum = add(1, piNumber); let c = new ComplexNumber(1, 2);
{
and}
when accessing a named export. Think of all named exports as part of an export object (as it was like this with CommonJS modules), an in a similar fashion, we can import every named export into an object, as in the exampleimport * as mathlib from './lib.js'
. Theas
keyword can be used both when exporting named exports and also when importing named exports. We should not use{
and}
when accessing a default export. - A module can be declared by using
export
inside the fileCustomer.js
in thecrm
folder, likeand accessed from outside the module by usingchargeCreditCard(cardNumber, amount) { //chargeCreditCard() is inaccessible from outside this module } class Customer { constructor(id) { ... } buy(item) { ... chargeCreditCard(this.cardNumber, item.price); ... } } export Customer;
import
, likeThis is similar to theimport {Customer} from './crm/Customer'; let customer = new Customer(123); let item = {productId: 456, productName: "Watch", price: 100}; customer.buy(item);
iife
(immediately invoked function expression) module definition and usage ines5
, like(function(target) { chargeCreditCard(cardNumber, amount) { //chargeCreditCard() is inaccessible from outside this module } function Customer(id) { ... } Customer.prototype = { buy: function(item) { ... chargeCreditCard(this.cardNumber, item.price); ... } } }(window)) var customer = new Customer(123); var item = {productId: 456, productName: "Watch", price: 100}; customer.buy(item);
- A module may export multiple classes, functions or variables, like
or like
function f() {...} class C {...} const pi = 3.14159; export f, C, pi;
andexport function f() {...} export class Customer {...} export const pi = 3.14159;
import
and use whatever we need, likeWe can also useimport {Customer, pi} from './crm/Customer'; let customer = new Customer(); let circumference = 2 * pi * 10;
export default
, likeand import it and use it with any name we want, likeclass VIPCustomer {...} export default VIPCustomer;
import Customer from './crm/VIPCustomer'; let customer = new Customer();
- An
async
function does not return it's result immediately, but instead returns aPromise
object. APromise
object can be in 3 states, it starts inpending
state, and when/if itresolve
s it switches tofulfilled
state, and when/if an error occurs, it switches torejected
state. Promises can be chained, likeThis code above will print "MyCompany" after 3 seconds (function getOrder(orderId) { //returns a Promise, starts a time taking operation and resolves with the result when the operation succeeds return new Promise((resolve, reject) => { setTimeout(() => resolve({id: 123, name: "Apple Macbook", userId: 456}), 1000); }); } function getUser(userId) { //returns a Promise, starts a time taking operation and resolves with the result when the operation succeeds return new Promise((resolve, reject) => { setTimeout(() => resolve({id: 456, name: "John Doe", companyId: 789}), 1000); }); } function getCompany(companyId) { //returns a Promise, starts a time taking operation and resolves with the result when the operation succeeds return new Promise((resolve, reject) => { setTimeout(() => resolve({id: 789, name: "MyCompany"}), 1000); }); } getOrder(123) .then(order => getUser(order.userId)) .then(user => getCompany(user.companyId)) .then(company => console.log(company.name)) .catch(error => console.log(error.message));
getOrder()
,getUser()
, andgetCompany()
each takes 1 second to complete). Note that.catch(error => console.log(error.message))
works the same way as.then(undefined, error => console.log(error.message))
does, but cleaner semantics. If we changegetUser()
to the following code,Then the samefunction getUser(userId) { //returns a Promise, starts a time taking operation and rejects with an Error result when the operation fails return new Promise((resolve, reject) => { setTimeout(() => reject(new Error("An error occured while fetching user with id: " + userId)), 1000); }); }
getOrder(123).then(...).then(...).then(...).catch(...)
chain call will print "An error occured while fetching user with id: 456" after 2 seconds Promise.all()
resolves multiple promises, likeThis code above will print all 5 company names after 1 second (each call to getCompany() takes 1 second, but calls are done in parallel). If we changefunction getCompany(companyId) { let companies = { 1: {id:1, name: "Apple"}, 2: {id:2, name: "Google"}, 3: {id:3, name: "Facebook"}, 4: {id:4, name: "Amazon"}, 5: {id:5, name: "Microsoft"} } //Promise.resolve could be used if we wanted to resolve immediately //return Promise.resolve(companies[companyId]); //returns a Promise, starts a time taking operation and resolves with the result when the operation succeeds return new Promise((resolve, reject) => { setTimeout(() => resolve(companies[companyId]), 1000); }); } let promises = [getCompany(1), getCompany(2), getCompany(3), getCompany(4), getCompany(5)]; Promise.all(promises) .then(companies => companies.forEach(company => console.log(company.name))) .catch(error => console.log(error.message));
getCompany()
to the following code,Then the samefunction getCompany(companyId) { //returns a Promise, starts a time taking operation and rejects with an Error result when the operation fails return new Promise((resolve, reject) => { setTimeout(() => reject(new Error("An error occured while fetching company with id: " + companyId)), 1000); }); }
Promise.all(promises).then(...).catch(...)
chain call will print "An error occured while fetching company with id: 1" after 1 second.Promise.race()
resolves the fastest completing promise. The following code will print "Apple" after 1 second, likelet promises = [getCompany(1), getCompany(2), getCompany(3), getCompany(4), getCompany(5)]; Promise.race(promises) .then(company => console.log(company.name)) .catch(error => console.log(error.message));
async
function wraps its return value in a Promise object, soasync f() {... return 123;}
is the same asf() {... return new Promise(...resolve(123)...);}
- This is similar to a C# async method returning a
Task<int> object
wrapping its return value of typeint
- await resolves a Promise object, so await p resolves the Promise object p, and await f() resolves the Promise returned by function f()
- When an async function returns a Promise, js flattens it out to a single Promise, so
async f() {... return new Promise(...resolve(123)...);}
is the same asasync f() {... return 123;}
- async and await are frequently used together in the async/await pattern like
x = await f();
but using with promises, they don't have to be used together all the time - Refer to https://dev.to/codeprototype/async-without-await-await-without-async--oom for more details
- To run two async functions in parallel, you cannot use
await f1(); await f2();
, instead either usePromise.all()
orp1 = f1(); p2 = f2(); await p1; await p2;
(starts f1() and f2() and resolves both later - Refer to https://medium.freecodecamp.org/avoiding-the-async-await-hell-c77a0fb71c4c for more details
- Refer to https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch for more details and examples
//basic get fetch('https://api.github.com/users/KrunalLathiya') .then(response => response.json()) .then(data => { console.log(data) //prints result from `response.json()` in getRequest }) .catch(error => console.error(error)) //cors fetch('https://api.github.com/users/KrunalLathiya', { credentials: 'include', //useful for including session ID (and, IIRC, authorization headers) }) .then(response => response.json()) .then(data => { console.log(data) //prints result from `response.json()` }) .catch(error => console.error(error)); //post data fetch('https://jsonplaceholder.typicode.com/users', { headers: { "Content-Type": "application/json; charset=utf-8" }, method: 'POST', body: JSON.stringify({ username: 'Elon Musk', email: 'elonmusk@gmail.com', }) }) //post data fetch('https://appdividend.com/api/v1/users', { credentials: 'same-origin', // 'include', default: 'omit' method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: JSON.stringify({user: 'Krunal'}), //coordinate the body type with 'Content-Type' headers: new Headers({ 'Content-Type': 'application/json' }), }) .then(response => response.ok() response.json()) .then(data => console.log(data)) //result from the `response.json()` call .catch(error => console.error(error)) //delete data fetch('https://jsonplaceholder.typicode.com/users/1', { method: 'DELETE' }); //use async/await let res = await fetch('https://api.github.com/users/KrunalLathiya'); let data = await res.json(); console.log(data); //upload file const formData = new FormData() const fileField = document.querySelector('input[type="file"].avatar') formData.append('username', 'abc123') formData.append('avatar', fileField.files[0]) fetch('https://appdividend.com/api/v1/users', { method: 'POST', //'GET', 'PUT', 'DELETE', etc. body: formData //coordinate the body type with 'Content-Type' }) .then(response => response.json()) //upload multiple files <input type='file' multiple class='files' name='files' /> const formData = new FormData() const fileFields = document.querySelectorAll('input[type="file"].files') // Add all files to formData``` [].forEach.call(fileFields.files, f => formData.append('files', f)) //alternatively for PHP peeps, use `files[]` for the name to support arrays //Array.prototype.forEach.call(fileFields.files, f => formData.append('files[]', f)) fetch('https://appdividend.com/api/v1/users', { method: 'POST', //'GET', 'PUT', 'DELETE', etc. body: formData //coordinate the body type with 'Content-Type' }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error))