DEV Community

loading...

Notes on ECMAScript 6 (ES6)

Scott Hardy
I like code
Updated on ・10 min read

Introduction

This is not meant to replace the official documentation.

This post does not cover all the ES6 features.

For typos and corrections: https://github.com/hardy613/es6-notes/issues

ES6 Variables

var vs let

Traditionally the keyword var initializes the identifier with a value:

var my_variable = 'value';
//1 //2         //3 

//1 the var keyword
//2 the identifier
//3 the value
Enter fullscreen mode Exit fullscreen mode

There are rules for naming the variable identifier. These are:

  • identifiers cannot be keywords
  • can be alphanumeric, although cannot start with a number
  • $ and _ are also allowed characters for an identifier

Variables decalred by var have the scope of the entire function.

function myFunc() {
    if(true) {
        var my_var = 'test';
    }
    console.log(my_var); // test
}
Enter fullscreen mode Exit fullscreen mode

The let keyword

let is preferred over var. Variables decalred by let have their scope
within the block they are defined.

function myFunc() {
    if(true) {
        let my_var = 'test';
    }
    console.log(my_var); // TypeError
}
Enter fullscreen mode Exit fullscreen mode

Block scoping allows for variable shadowing.

function myFunc() {
    let my_var = 'test';
    if(true) {
        let my_var = 'new test';
        console.log(my_var); // new test
    }
    console.log(my_var); // test
}
Enter fullscreen mode Exit fullscreen mode

The const keyword

ES6 also introduced a new variable keyword: const. Variables declared with
the const keyword are block scoped just like let however they cannot
change by reassignment and they cannot be re-declared; they are immutable.

const version = '0.0.1';
version = '0.0.2'; // TypeError: invalid assignment to const

const name = 'bill';
const name = 'ted'; // SyntaxError: Identifier 'name' has already been declared
Enter fullscreen mode Exit fullscreen mode

Variables declared by const (constants) cannot be changed. However, with a
for loop the scope is redeclared at the start of each loop, where a new
const can be initalized.


function myFunc(items) {
    for(let i = 0; i < items.length; i++) {
        const message = items[i] + ' found at index: ' + i;
        console.log(message);
    } 
}

myFunc(['test', 100, 200]);
// test found at index: 0
// 100 found at index: 1
// 200 found at index: 2
Enter fullscreen mode Exit fullscreen mode

ES6 for/of

The for/of loop uses the iterable protocol to create a loop. Strings, Arrays, TypedArray, Map, Set, NodeList, and custom iterable function hooks can all be used with for/of.

const arr = [1, 2, 3];
for(const number of arr) {
    console.log(number) // 1 2 3
}
Enter fullscreen mode Exit fullscreen mode

To iterate over an object you can use the protocol Object.entries().
This will give arrays of ['key', 'value'] pairs. Unlike for/in this will
not iterate through the object prototype

const obj = { a:1, b:2, c:3 };
for(const prop of Object.entries(obj)) {
    console.log(prop); // ['a', 1] ['b', 2] ['c', 3]
}
Enter fullscreen mode Exit fullscreen mode

ES6 Template Literals

Template literals are very handy for strings that use variables, or need to
make use of a quick javascript expression. Template literals are enclosed with
the back-tick. Template literals can also have placeholders,
these are declared with a dollar sign and curly braces ${placeholder}.

const number = 42;
const str = `Here's my favourite number: ${number}.`;
console.log(str) // Here's my favourite number: 42.

const count = 0;
console.log(`${count + 1}`); // 1 
Enter fullscreen mode Exit fullscreen mode

Template literals can be tagged with a function identifier before the
back-ticks. The function allows you to parse the template literal. The first
argument is an array of string values, the rest of the arguments relate to
the placeholders in the template literal.

const name = 'Theodor Logan';
const age = 21;

function showNameAndAge(strings, nameHolder, ageHolder) {
    // strings[0] is empty because we started with a
    // ${name} placeholder, placeholders at the start or 
    // at the end of a template literal will have
    // an empty string before or after respectively 
    const piece1 = strings[1]; // is
    const piece2 = strings[2]; // years of age.
    let ageNotice = '';
    if(ageHolder < 25) {
        ageNotice = 'What a babyface. ';
    } else {
        ageNotice = 'What an oldtimer. ';
    }
    return `${ageNotice}${nameHolder}${piece1}${ageHolder}${piece2}`;
}

showNameAndAge`${name} is ${age} years of age.` 
// What a babyface. Theodor Loagn is 21 years of age.
Enter fullscreen mode Exit fullscreen mode

Tagged templates literals do not need to return a string.

ES6 Arrow Functions

Arrow functions are a shorthand syntax for functions that do not contain its
own this, arguments, super, or new.target and cannot be used as
constructors.

const arr = ['hammer', 'nails', 'pizza', 'test'];
console.log(arr.map(value => value.length)); // [6, 5, 5, 4]
Enter fullscreen mode Exit fullscreen mode

Arrow functions are useful for anonymous functions,
however their power is with the lexical scoping of this.

function es6LexicalScope() {
    this.timeSpentSeconds = 0;
    setInterval(() => {
        console.log(this.timeSpentSeconds++); // 1 2 3 ...
    }, 1000);
}
es6LexicalScope();
Enter fullscreen mode Exit fullscreen mode

Arrow functions do not have a prototype.

const func = () => {};
console.log(func.prototype); // undefined
Enter fullscreen mode Exit fullscreen mode

To return an object as an implicit return, you can wrap the object in
the grouping operator (parentheses).

const returnObjBad = () => { test: 'value' };
console.log(returnObj); // undefined

const returnObjGood = () => ({test: 'value'});
console.log(returnObj); // { test: 'value' }
Enter fullscreen mode Exit fullscreen mode

If you noticed, there is a small difference between the usage of arrow
functions in the provided exmaples. The usage of ():

  • Arrow functions with no parameters require ()
  • Arrow functions with one parmeter () are optional
  • Arrow functions with two or more parameters require ()
  • Arrow functions that only return, do not need {}, return, or ;
const fn1 = () => {[Native Code]};
const fn2 = param => {[Native Code]};
const fn2a = (param) => {[Native Code]};
const fn3 = (param1, param2) => {[Native Code]};
const fn4 = param => param;
Enter fullscreen mode Exit fullscreen mode

ES6 Destructuring Assignment

Destructuring assignment lets you unpack values from an array or object.

const [x, y] = [1, 2, 3, 4, 5];
console.log(x); // 1
console.log(y); // 2;

const person = { name: 'Bill', age: 42, email: 'bill@example.ca', url: 'http://example.ca' };
const {name, age} = person;
console.log(name, age); // Bill, 42
Enter fullscreen mode Exit fullscreen mode

Sometimes you want to keep all the other stuff. That is where the spread
operator ... comes in handy.

const [x, y, ...allTheRest] = [1, 2, 3, 4, 5];
console.log(x, y, allTheRest); // 1, 2, [3, 4, 5]

const person = { name: 'Bill', age: 42, email: 'bill@example.ca', url: 'http://example.ca' };
const {name, age, ...details} = person;
console.log(name, age, details); // Bill, 42, {email: 'bill@example.ca', url: 'http://example.ca'}
Enter fullscreen mode Exit fullscreen mode

You can also destructure to build new variables!

const otherObj = {};
const person = { name: 'Bill', age: 42, email: 'bill@example.ca', url: 'http://example.ca' };
const obj = {...otherObj, person};
console.log(obj); // { person: {[...]} }
Enter fullscreen mode Exit fullscreen mode

obj now has our person property with our person Bill. If the person
property was already set in otherObj then we would override that property.
Let's look at unpacking the length property from a string with destructuring.

const arr = ['hammer', 'nails', 'pizza', 'test'];
// without destructuring
console.log(arr.map(value => value.length)); // [6, 5, 5, 4]
// with destructuring
console.log(arr.map(({ length }) => length)); // [6, 5, 5, 4]
Enter fullscreen mode Exit fullscreen mode

Let's breakdown the line we just added. console.log(arr.map( is pretty
standard. ({ length }) is the parameter for our arrow function, we are passing
in a string and destructuring the length property from the string and passing
that as a variable called length. The function parameter is the string
length. => length)); the rest of our arrow function. The property is also
the variable identifier and we only return the length. If you need a default
with destructuring, you can do that too!

const { name = 'Bill', age = 30 } = { name: 'Ted' };
console.log(name, age)// Ted, 30

const [x = 5, y = 10] = [20];
console.log(x, y) // 20, 10
Enter fullscreen mode Exit fullscreen mode

ES6 Default Parameters

Funtions accept default parameters and destructuring parameters.

function addToFive(addTo = 0) {
    return addTo + 5;   
}
const ex1 = addToFive();
const ex2 = addToFive(5);
console.log(ex1, ex2); // 5, 10

function fullname ({firstname, lastname}) {
    return `${firstname lastname}`;
}
const user = { firstname: 'Theodore', lastname: 'Logan', age: '20' };
const fullname = fullname(user);
console.log(`Hello ${fullname}`);
Enter fullscreen mode Exit fullscreen mode

When destructuring you can also assign defaults.

function myFunc({age = 42}) {
    console.log(age); // 42
};
myFunc({name: 'Theodor'});
Enter fullscreen mode Exit fullscreen mode

ES6 Classes

ES6 class is new syntax for the traditional classes introduced in ES2015.
ES6 Classes are not introducing anything to JavaScript rather just another way
to write a JavaScript class
. Class bodys are subject to JavaScript's
strict mode, the class body has new keywords and some words are
reserved as keywords for future use.

As with functions there are two ways to declare a class, expression or
declaration.

// expression
const Instrument = class {}; // or class Instrument {}
const instrument = new Instrument();

// declaration
class Instrument {}
const instrument = new Instrument();
Enter fullscreen mode Exit fullscreen mode

Unlike a function, a class must be declared or expressed before it can used.

Constructors

constructor is a reserved keyword for classes and represent a function that
is called during the creation and initialization.

class Instrument {
    constructor(props) {
        this._make = props.make;
        this._type = props.type;
    }

    get type() {
        return this._type;
    }
}

const noiseMaker = new Instrument({ make: 'Crafter', type: 'Guitar' });
console.log(noiseMaker.type); // Guitar
Enter fullscreen mode Exit fullscreen mode

Getters and Setters

getters and setters allow read and write access to class properties without
having to define methods. Getters and setters are accessible by inherited
classes.

class Instrument {
    constructor(props) {
        this._make = props.make;
        this._type = props.type;
    }

    set make(make) {
        this._make = make;
    }

    get make() {
        return this._make;
    }

    set type(type) {
     this._type = type;
    }

    get type() {
        return this._type;
    }

}

const noiseMaker = new Instrument({ make: 'Crafter', type: 'Guitar' });
noiseMaker.type = 'Drums';
noiseMaker.make = 'Yamaha';
console.log(noiseMaker.type); // Drums
Enter fullscreen mode Exit fullscreen mode

Inheriting

Classes can inherit a parent class. Keeping with Instruments, let's make a
guitar class. The super keyword refers to the class being inherited.

class Guitar extends Instrument {
    constructor(make) {
        super({make, type: 'Guitar'});
    }
    set make (make) {
        super.make = make
    }
    get make() {
        return `The make of the guitar is: ${super.make}`;
    }
}

const myGuitar = new Guitar('Fender');
console.log(myGuitar.make); // The make of the guitar is: Fender
myGuitar.make = 'Crafter';
console.log(myGuitar.make); // The make of the guitar is: Crafter
console.log(myGuitar.type); // Guitar
Enter fullscreen mode Exit fullscreen mode

Methods

Class methods are functions with the function keyword dropped.

class Guitar extends Instrument {
    constructor(make) {
        super({make, type: 'Guitar'});
    }

    set make (make) {
        super.make = make
    }

    get make() {
        return `The make of the guitar is: ${super.make}`;
    }

    log() {
        console.log(this.make, this.type);
    }
}

const fender = new Guitar('Fender');
fender.log(); // The make of this guitar is: Fender, Guitar
Enter fullscreen mode Exit fullscreen mode

Object Definitions

Currently our object .toString() definition would return [object Object].
We can change the definition with a method property.

class Guitar extends Instrument {
    constructor(make) {
        super({make, type: 'Guitar'});
    }

    set make (make) {
        super.make = make
    }

    get make() {
        return `The make of the guitar is: ${super.make}`;
    }

    toString() {
        return `[${super.name} ${this.type}]`;
    }
}

const fender = new Guitar('Fender');
console.log(fender.toString()); // [Instrument Guitar]
Enter fullscreen mode Exit fullscreen mode

super and this

Before you can use this.property in a constructor of an inherited class, you
must call super() first.

class Guitar extends Instrument {
    constructor(make, stringCount) {
        super({make, type: 'Guitar'});
        this._stringCount = stringCount || 6;
    }

    set make (make) {
        super.make = make
    }

    get make() {
        return `The make of the guitar is: ${super.make}`;
    }

    get stringCount() {
        return this._stringCount;
    }

    set stringCount(stringCount) {
        this._stringCount = stringCount;
    }
}

const guitar = new Guitar('Fender', 12);
console.log(guitar.stringCount); // 12
Enter fullscreen mode Exit fullscreen mode

ES6 Modules

ES6 modules use the import and export keywords and are intended to be used
with the browser or with a server environment like NodeJs

// utils.js
export function add(left = 0, right = 0) {
    return left + right;    
};

export function times(left = 0, right = 0) {
    return left * right;
}
Enter fullscreen mode Exit fullscreen mode

Now we can import our utils file. There are a few ways we can import.

// index.js
import * as utils from './utils.js'
// utils.add(), utils.times()

import { add, times } from './utils.js'
// add(), times()
Enter fullscreen mode Exit fullscreen mode

You can also export variables or objects.

// my-module.js

const myVariable = 100;

const person = {
    name: 'Bill',
    age: 42
};

function trim(string = '') {
    return typeof string === 'string' && string.trim();
};

export { myVariable, person, trim };

// index.js
import { myVariable as maxAge, person, trim } from './my-module.js';

console.log(maxAge, person.age); // 100, 42

trim(' test '); // 'test'
Enter fullscreen mode Exit fullscreen mode

There are two different types of export, named and default. You can have
multiple named exports in a module but only one default export. The above
examples are all from the named export, let's take a look at the default
export syntax.

// a default funtion
export default function() {[...]}
export default function myFunc() {[...]}

// a default class
export default class MyClass {[...]}
Enter fullscreen mode Exit fullscreen mode

You can also have a variable as a default export

// other-module.js
const mySuperLongNamedVariable = 100;
export default mySuperLongNamedVariable;
Enter fullscreen mode Exit fullscreen mode

When importing defaults you can name them without the * as keyword.

// index.js
import theVariable from './other-module.js'
console.log(theVariable); // 100
Enter fullscreen mode Exit fullscreen mode

ES6 Promises

A Promise is an object representing the eventual completion or failure of an
asynchronous operation.

Working with promises

Promises are a convenient way to organize the order of operation for your
program and provide and alternative to passing callbacks as function parameters.
Say we have a function callToDb that makes a database call and returns a
promise

function success(result) {
    // do something with result
}

function failed(error) {
    // do something with error
}

callToDb('table_name').then(success, failed);
Enter fullscreen mode Exit fullscreen mode

failed is only called if an Error is returned. Both of these arguments are
optional, however to use the result of the previous promise you need at least
a success function with one argument


callToDb('table_name')
    .then(response => {
        // do something with response
    })
    .catch(error => {
        // do something with error
    });
Enter fullscreen mode Exit fullscreen mode

Like the above failed function, catch is only called if an Error is
returned. then returns a promise meaning we can now create a promise chain.


callToDb('table_name')
    .then(response => {
        // do something with response
        response.changesMade = true;
        return response;
    })
    .then(response => {
        // do more work
    })
    .catch(error => {
        // do something with error
    });
Enter fullscreen mode Exit fullscreen mode

Chains can be as long as you need them. catch can also be used multiple
times in a promise chain, the next catch in the chain is called on return
of an Error and following thens will still be called.


callToDb('table_name')
    .then(response => {
        // do something with response
        response.changesMade = true;
        return response;
    })
    .then(response => {
        // do more work
    })
    .catch(error => {
        // only called for above thens
    })
    .then(response => {
        // do more work
        // will still happen after the catch, even if catch is called
    })
    .catch(error => {
        // do something with error
        // only called for the one above then if an Error is returned
    });
Enter fullscreen mode Exit fullscreen mode

Creating a promise

The promise constructor should only be used to to wrap a function that does not
support a promise. Most libraries have built-in support for promises which
enable you to start chaining then right out of the box without a promise
constructor.

The promise constructor takes one executor function with two arguments:
resolve and reject. Let's create callToDb, a wrapping function to a
function without promise support.


function callToDb(table_name) {
    return new Promise((resolve, reject) => {
        return db_orm(`select * from ${table_name}`, (err, res) => {
            if(err) {
                reject(err);
            } else {
                resolve(res);
            }
        })
    });
}
Enter fullscreen mode Exit fullscreen mode

A few things are happening here:

  • db_orm is our database library without promise support, it takes a callback
  • wrapping db_orm is our returning Promise which has our executor function with resolve and reject
  • once db_orm is in the callback we reject with the error, this will trigger a catch or
  • we resolve with our result, this will trigger the next then

Reject

Reject returns a promise that is rejected with a reason. To debug with ease
it is recommended to make the reason an instance of Error

Promise.reject(new Error('My custom message'))
    .then(result => {
        // not called
    })
    .catch(result => {
        console.log(result); // Error: My custom message
    })
Enter fullscreen mode Exit fullscreen mode

To reject a promise inside a then chain you can return a new Error or
throw an Error to the catch.

Resolve

Resolve returns a promise that is resolved with a result. result can also
be another promise, thenable or value.

Promise.resolve('Sweet!')
    .then(result => {
        console.log(res); // Sweet!
    })
    .catch(result => {
        // not called
    });
Enter fullscreen mode Exit fullscreen mode

Thanks to Hannah and Jonathan for helping proof read and thank you for reading.

I hope this helps!

edits: To code blocks

Cheers.

Discussion (21)

Collapse
pwaivers profile image
Patrick Waivers

Great write-up! What are your opinions on using const over let or var? What are the actual advantages of it (other than telling the developer that it will not change)?

Collapse
hardy613 profile image
Scott Hardy Author • Edited

Hey Patrick!

Thanks for the great question. I will try to answer by breaking it down first with var vs let and const

var is function scoped. let and const are block scoped. This is the biggest difference, to use the wise words of a work colleague: "let is what var should have been from the start."

That said the main difference with let vs const is that const does not allow re-declaring. This does tell a developer that it will not change as you mentioned, but more importantly, it is telling your program that it cannot change.

Some linters out there (AirBnB) will throw an error if you declare a let and never change or re-declare it, the error will be to use a const.

I hope I answered your question sufficiently.

Cheers!

Collapse
flaviocopes profile image
flavio ⚡️🔥

const has a great advantage: there are less moving parts in your code.

I now default to const unless I know the variable is going to be reassigned later on.

Collapse
hardy613 profile image
Scott Hardy Author

Patrick,

I was re-reading my post and noticed I am inconsistent with my let and const usage. I opened an issue and I am welcome to PR's

Again thank you for reading and asking a question.

Collapse
ben profile image
Ben Halpern

The scoping component of let seems pretty beneficial. let and const seem to remove some of the wishy-washiness of JS.

Collapse
hardy613 profile image
Scott Hardy Author

I totally agree. Block scoping is great progress

Collapse
tunaxor profile image
Angel D. Munoz

Great notes!
If I may add: regarding to classes you might want to try something like this if you want to accept an options like constructor

class SomeClass {
  constructor(options) {
    Object.assign(this, options);
    // if you want required properties you may want to if check here
    // and throw if required props are not present
  }

  toString() {
    return Object.entries(this)
      .map(([key, value]) => `[${key}: ${value}]`)
      .reduce((last, current) => `${last ? `${last}, ` :  ''}${current}`,'')
  }
}


const some = new SomeClass({propA: 'this is a', propB: 'this is b'});
some.toString();
//"[propA: this is a], [propB: this is b]"

of course if you are using typescript you can extend that to use an interface and ensure you only accept what is required.

Collapse
hardy613 profile image
Scott Hardy Author

What are your thoughts on iterator as a protocol and not an interface?

Collapse
tunaxor profile image
Angel D. Munoz

it is pretty useful, I think it has become better to work with collections, if you have teamates with python background, they can easily recognize [...someIterable] also it lets you do some stuff like const keys = [...Object.keys(myObj)] without for in or for each
also for people that isn't too much into functional methods like map, reduce, ans such, for of is quite a savior.

on the protocol vs interface, I think protocol is better suited in jsland where interfaces only exist on agreements (which some times not everyone agrees the same thing), not enforced in code.

Collapse
ikirker profile image
Ian Kirker

As someone who tries to write as little JavaScript as possible, I'm not sure I'm looking forward to infinite chains of impossible-to-debug promises any more than I currently enjoy infinite chains of impossible-to-debug callbacks.

Collapse
hardy613 profile image
Scott Hardy Author • Edited

Hey Ian,

Thanks for taking time to leave a comment, what part of a Promise Chain are you finding impossible to debug? Promises offer a second failed function within a then or the use of a catch to manage errors. Maybe I can help clear up some confusion.

In my opinion a promise chain is relief from callback hell.

Collapse
ikirker profile image
Ian Kirker

I've not actually tried using them yet, but my comment mostly comes from looking at that chain, imagining a longer one, and then imagining trying to work out which .then and .catch happen at which level, much like trying to work out which level of }) } } }) ) }, }; } } }) the problem is in with callback hell.

I guess it should at least be easier to add good error reporting in the .catch blocks.

Thread Thread
hardy613 profile image
Scott Hardy Author

I recommend trying promises out, start by working with promises before creating them. A promise that you could work with is the fetch api for example, google has a good introduction

const getJson = res => {
    // comment out the if to return the error 
    // and see how catch works
    if(res.status !== 200) {
        return new Error(`StatusCode: ${res.status}`)
    }
    return res.json()
}
const getUrl = url => fetch(url).then(getJson)

getUrl('https://baconipsum.com/api/?type=meat-and-filler')
    .then(res => console.log(res))
    .catch(err => console.error(err.message))
Collapse
yashwanth2804 profile image
kambala yashwanth

Great post, can you change code block representation of example code to runkit.

Collapse
hardy613 profile image
Scott Hardy Author

Hey Kambala!

I am unfamiliar with runkit. Feel free to fork the repo and submit a PR though as an example. I might be able to do this with your help. The project is open source

Collapse
yashwanth2804 profile image
kambala yashwanth

pull request.Please check out.

Thread Thread
hardy613 profile image
Scott Hardy Author

On a work trip. I'll check it out this weekend. Thanks for your work!

Collapse
c0il profile image
Vernet Loïc

What is nice with js is that every day you learn a new thing. I never used const before, nice. Thanks for those tips. :)

Collapse
vasilevskialeks profile image
Aleksandar Vasilevsk

Great article, feel free to check my article on ES5: codespot.org/javascript-101-es6-an...