Skip to content

Latest commit

 

History

History
754 lines (589 loc) · 18.1 KB

File metadata and controls

754 lines (589 loc) · 18.1 KB

ES6 Variables

var vs let

A variable in javascript is a named space in memory. 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

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
}

The let keyword

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

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

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
}

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

Variables decalred 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]);

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 (let number of arr) {
  console.log(number); // 1 2 3
}

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 (let prop of Object.entries(obj)) {
  console.log(prop); // ['a', 1] ['b', 2] ['c', 3]
}

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}.

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

let count = 0;
let displayCount = `${count + 1}`;
console.log(displayCount); // 1

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.

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

function showNameAndAge(strings, nameHolder, ageHolder) {
	// string[0] is empty as we started the template literal
	// with a ${name} placeholder. Placeholders at the start
	// or end of a template literal with have an empty string
	// before or after respectively
	let piece1 = strings[1]; // is
	let piece2 = string[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.

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.

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

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();

Arrow functions do not have a prototype.

let func = () => {};
console.log(func.prototype); // undefined

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

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

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

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 ;
let fn1 = () => {[Native Code]};
let fn2 = param => {[Native Code]};
let fn2a = (param) => {[Native Code]};
let fn3 = (param1, param2) => {[Native Code]};
let fn4 = param => param;

ES6 Destructuring Assignment

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

let [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',
};
let { name, age } = person;
console.log(name, age); // Bill, 42

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

let [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',
};
let { name, age, ...details } = person;
console.log(name, age, details); // Bill, 42, {email: 'bill@example.ca', url: 'http://example.ca'}

You can also destructure to build new variables!

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

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.

let 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]

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!

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

let [x = 5, y = 10] = [20];
console.log(x, y); // 20, 10

ES6 Default Parameters

Funtions accept default parameters and destructuring parameters.

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

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

When destructuring you can also assign defaults.

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

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 {}
let instrument = new Instrument();

// declaration
class Instrument {}
let instrument = new Instrument();

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 a constructor initialization.

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

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

let noiseMaker = new Instrument({ make: 'Crafter', type: 'Guitar' });
console.log(noiseMaker.type); // Guitar

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;
  }
}

let noiseMaker = new Instrument({ make: 'Crafter', type: 'Guitar' });
noiseMaker.type = 'Drums';
noiseMaker.make = 'Yamaha';
console.log(noiseMaker.type); // Drums

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' });
  }

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

let 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

Methods

Class methods are functions with the function keyword dropped.

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

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

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

let fender = new Guitar('Fender');
fender.log(); // The make of this guitar is: Fender, Guitar

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' });
  }

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

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

let fender = new Guitar('Fender');
console.log(fender.toString()); // [Instrument Guitar]

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;
  }

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

  get stringCount() {
    return this._stringCount;
  }

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

let guitar = new Guitar('Fender', 12);
console.log(guitar.stringCount); // 12

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;
}

Now we can import our utils files. 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()

You can also export variables or objects.

// my-module.js

const myVariable = 100;

let 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'

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 {[...]}

You can also have a variable as a default export

// other-module.js
let mySuperLongNamedVariable = 100;
export default mySuperLongNamedVariable;

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

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

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);

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
  });

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
    let res = (response.changesMade = true);
    return res;
  })
  .then(response => {
    // do more work
  })
  .catch(error => {
    // do something with error
  });

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
    let res = (response.changesMade = true);
    return res;
  })
  .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
  });

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);
      }
    });
  });
}

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
  });

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
  });

Source: https://github.com/shardy613/es6-notes