JavaScript advanced features

Table of Contents

New Features

  • New versions of JavaScript, such as ES6, ES2016, 2017, etc., come with many new features, however it is still ECMAScript
  • Many of the new features are syntactic sugar or extend the functionality only marginal

Template Literals

  • Template literals are string literals with support for text interpolation and multiple lines
  • Template literals are enclosed by backticks
  • Expressions in placeholders (marked by the dollar sign and curly braces) are evaluated and replaced by their value at runtime
function getYear() {
    return (new Date()).getFullYear();
}

const student = {
    name: "Bob",
    age: 25,
    university: "BFH"
};

const message = `${student.name} is a student at ${student.university}.
${student.name} was born in ${getYear() - student.age}.`;

console.log(message);    // >> Bob is a student at BFH.
                         //    Bob was born in 1996.

Spread Operator

  • Using the spread operator new arrays and objects can be created based on the values of other arrays or objects
  • The spread operator is especially useful to shallow-clone and merge or extend objects
const a = [1, 2];
const b = [3, 4, 5];

const clonedA = [...a];
clonedA[1] = 3;                         // a remains unaffected

const mergedArr = [0, ...a, ...b, 6];   // [0, 1, 2, 3, 4, 5, 6]

const x = {a: "foo", b: 7};
const y = {a: "bar", c: 8};

const clonedX = {...x};
clonedX.a = "bar";                      // x remains unaffected

const mergedObj = {...x, ...y, c: 9};   // \{a: "bar", b: 7, c: 9\}

Array and Object Destructuring

  • The destructuring assignment unpacks values from arrays and objects into distinct variables
  • Destructuring can also be used in function parameter definitions to unpack fields from objects passed as argument
const a = [1, 2, 3, 4];
const [first, second] = a;
console.log(first);             // >> 1
console.log(second);            // >> 2

const x = {a: "foo", b: "bar", c: 12};
const {b, c} = x;
console.log(b);                 // >> bar
console.log(c);                 // >> 12

function f({a, b}) {
    console.log(a+", "+b);
}
f(x);                          // >> foo, bar

Arraw functions

  • Arrow functions are a compact alternative to function expressions
  • If the body consists of a single expression, then the curly braces can be omitted and the return is implied
const f = (a, b) => {
  const c = a * a;
  return c+b;
};
console.log(f(3, 4));         // >> 13

const g = a => a * a;
console.log(g(3));            // >> 9
  • Arrow functions cannot be bound to objects
  • The this reference is lexically scoped
const alice = {
  name: "Alice",
  friends: ["Bob", "Eve"],
  sayHi: function() {
    this.friends.forEach(friend => {
      console.log(this.name+" says hi to "+friend);  // this is lexically scoped
    });
  }
};

const speak = phrase => console.log(this.name+" says "+phrase);
speak.call(alice, "Hello World!");   // >> undefined says Hello World!

Classes

  • The class keyword is syntactic sugar for constructor functions and prototype inheritance
  • A class may contain a constructor, the actual constructor function, which will be bound to the class name, and any number of methods
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  speak(phrase) {
    console.log(`${this.name} says: ${phrase}`);
  }
}

const alice = new Person("Alice", 19);
alice.speak("It's just syntactic sugar!");

Class Inheritance

  • Classes support inheritance
  • The super keyword is used to reference the constructor and methods defined in the base class
class Student extends Person {
  constructor(name, age, university) {
    super(name, age);
    this.university = university;
  }
  speak(phrase) {
    super.speak(phrase);
    console.log("And I'm a student at the "+this.university);
  }
}

Modules

Global Scope

  • JavaScript has one global scope
  • Different JavaScript files share the same global scope and have not their own private scope

let counter = 0;
function count() { return ++counter; }

function logCounter() { console.log(counter); }

<html>
<head>
<script src="listing_11a.js"></script>
<script src="listing_11b.js"></script>
</head>
<body>
<script>
count();
logCounter(); // >> 1
</script>
</body>
</html>

IIFE

  • An immediately-invoked function expression (IIFE, pronounced ”iffy”) is an unnamed function which is immediately invoked
  • An IIFE takes advantage of the concept of closures and the local scope produced by functions
  • IIFEs can be used to implement a simple module pattern, in which each module binds only a single global variable

counter-service.js:

const counterService = (function() {
    // Private scope
    let counter = 0;
    function count() { return ++counter; }
    function logCounter() { console.log(counter); }

    // Export public features
    return {count, logCounter};
})();

<script src="counter-service.js"></script>
<script>
counterService.count();
counterService.logCounter(); // 1
</script>

Example jQuery Plugin

  • A jQuery plugin is implemented as an IIFE
  • New features are added to the prototype of jQuery objects (which is the $.fn object)
  • See How to Create a Basic Plugin for more details

jquery-identify.js;

(function($) {
  let counter = 0;

  $.fn.identify = function() {
    return this.each(function() {
      this.id = 'id-'+(++counter);
    });
  }
})(jQuery);

<script src="jquery.js"></script>
<script src="jquery-identify.js"></script>
<script>
$('p, img').identify();
</script>

Module Systems

  • Module Systems provide a private scope on a file basis with the possibility to export functionality and to import functionality from other modules
  • Before ES6, JavaScript had no built-in module system and proprietary module systems, such as CommonJS (which is still used by Node.js), were needed
  • ES6 introduced its own module system (often called ES modules) with the new keywords import and export
  • In larger applications, module dependencies are resolved during the build and bundling process
  • ES modules are natively supported by all modern browsers

ES Modules

  • Features are exported from a module using the export keyword
  • The export keyword is placed in front of the item to export
  • Functions, classes and variables defined with let or const can be exported
  • Multiple features can be exported in a single statement by wrapping the comma-separated list of items in curly braces
  • Items which are not exported are local to the module
  • Features are imported from other modules using the import keyword
  • Specific features are imported by their names (comma-separated list in curly braces)
  • All features of a module can be important using the *-notation

Export Examples

math.js:

export const PI = computePI();
export function square(x) { return x * x; }
export function sum(x, y) { return x + y; }
// Local to math.js
function computePI() { return 3.14; }

colors.js:

const white = '#ffffff', black = '#000000';
function rgbToHex(r, g, b) { return '#' + toHex(r) + toHex(g) + toHex(b); }
function toHex(c) { return ('0' + c.toString(16)).slice(m2); }
// Export multiple features in a single statement
export {white, black, rgbToHex};

Import Examples

file1.js:

// Import specific features from another module
import {square, PI} from 'math';
console.log(PI); // >> 3.14
console.log(square(4)); // >> 16

file2.js

// Import all features from another module
import * as math from 'math';
console.log(math.PI); // >> 3.14
console.log(math.sum(4, 5)); // >> 9

Default Export

  • One single export per module can be marked as default using the default keyword
  • The default feature is imported without curly braces and any name can be given
  • Especially convenient if only one feature is exported per module

hello-world.js:

export default function() {
console.log("Hello World!");
}

any-class.js:

export default class {
// ...
}

main.js:

import logHelloWorld from 'hello-world';
import AnyClass from 'any-class';
logHelloWorld();
let obj = new AnyClass();

Usage in the Browser

  • The type of the script must be module
  • Modules are loaded deferred (after the browser finished parsing the HTML and building the DOM)
  • Modules are running in strict mode by default
  • The module name must be the file path relative to the site root or, using the dot-syntax, relative to the current location, e.g.: import ... from './lib/my-module.js';

<html>
<head>
<title>ES Modules</title>
<script type="module" src="main.js"></script>
<script type="module">
import {PI} from './math.js';
// ...
</script>
</head>
<body> ... </body>
</html>

Promises

Asynchronous Programming

  • JavaScript is single-threaded, hence time-consuming tasks must be implemented asynchronously to not block the thread
  • Promises are helpers for asynchronous programming
  • A promise is a proxy for a value possibly known in the future
  • Instead of passing callback functions to the asynchronous function, the  synchronous function returns a promise on which success and error handlers can be registered
// Traditional approach with callbacks passed to the asynchronous function
computeAsync("Foo", function(result) {
  // Process result
}, function(error) {
  // Handle error
});

// Promise based approach where the asynchronous function returns a promise
computeAsync("Foo")
  .then(result => { /* Process result */ })
  .catch(error => { /* Handle error */ });

Promises Are State Machines

  • Newly created promises are in the state pending
  • A promise remains pending as long as the value is unknown and no error has occurred
  • As soon as the value is available, the state changes to fullfiled and registered success handlers are called
  • If an error occurs, the state changes to rejected and registered error handlers are called
  • Once the state is fullfiled or rejected, the promise remains in this state for ever

Creating Promises

  • Promises are created using the Promise constructor
  • A function is passed to the constructor implementing the asynchronous task
  • The function is called by the promise, passing two functions to resolve or reject the promise
  • Either the resolve or reject function must be finally called by the asynchronous task
function computeAsync() {
  return new Promise((resolve, reject) => {

    // ... Perform the asynchronous task (Promise is pending)

    if (success) resolve(result);  // Promise will be fulfilled
    else reject(error);            // Promise will be rejected
  });
}

Promise Example

function computePI() {
  return new Promise((resolve, reject) => {
    // Computing PI is hard work...
    setTimeout(() => {
      // Computing PI fails with a certain probability...
      if (Math.random() < 0.2) {
        reject("Sorry, computation failed!");     // Reject the promise
      } else {
        resolve(3.14);       // Resolve the promise to the value of PI
      }
    }, 300);
  });
}

computePI()
  .then(result => console.log("PI: "+result))
  .catch(error => console.log(error));

Chaining

  • The then and catch functions return a promise, hence they can
    be chained
  • The promise returned by then and catch resolves to the return value of the handler or to its original settled value if the handler was not called (e.g. a catch-handler is not called on a fullfiled promise)
  • If the handler returns a promise, the promise returned by then and catch resolves to the same value as the returned promise

Chaining and Combining Examples

// Chaining
doAsync()
  .then(resultA => { /* ... */ })
  .then(resultB => { /* ... */ })
  .catch(error => { /* ... */ })
  .then(resultC => { /* ... */ })
  .catch(error => { /* ... */ });

// Add multiple handlers to the same promise
const promise = doAsync();
promise.then(result => { /* ... */ });
promise.then(result => { /* ... */ });

// 'Wait' until all promises are fulfilled or one rejected
Promise.all([doAsync("A"), doAsync("B"), doAsync2()])
  .then(results => { /* ... */ })
  .catch(error => { /* ... */ });

// 'Wait' until the first promise is fulfilled or rejected
Promise.race([doAsync("A"), doAsync("B"), doAsync2()])
  .then(result => { /* ... */ })
  .catch(error => { /* ... */ });

async and await

  • async and await allow asynchronous code to be written synchronously
  • An async function implicitly returns a promise, which resolves to the return value of the function or is rejected on exception
  • async functions can await other promises (written synchronously)
  • await interrupts the synchronous execution and waits until the returned promise is resolved or rejected
async function computeDiskArea(radius) {
  const PI = await computePI();
  return radius * radius * PI;
}

computeDiskArea(2)
  .then(area => console.log("The area of a disk with r=2 is "+area))
  .catch(error => console.log("An error occurred: "+error))

Fetch API

  • The Fetch API provides an interface for fetching resources
  • This includes resources across the network
  • It is similar to the XMLHttpRequest but it is more powerful and more flexible
  • It provides a generic definition of Request and Response objects
  • The global fetch function takes the URL of a resource as parameter and returns a promise which resolves to a Response object once the response is available

fetch('http://quotes.org')
  .then(response => response.text())
  .then(quote => console.log(quote));

Response Object

  • A Response object has the following properties:
    • ok - true if the status code is 2xx, false otherwise
    • status - the response status code
    • statusText - the status code message
    • headers - the response headers
  • In addition, Response provides the following methods:
    • text() returns a promise resolving to the body as string
    • json() returns a promise resolving to the result of parsing the body as JSON
    • blob() returns a promise resolving the body as Blob object

fetch('http://news.org')
  .then(response => response.json())
  .then(data => data.articles.forEach(article => console.log(article.title)));

Request Options

The fetch function has as a second optional parameter that allows setting the HTTP method, headers and request body:

fetch('http://news.org', {
method: 'POST',
headers: {
'Authorization:': 'Basic amRAZXhhbXBsZS5vcmc6MTIzNDU=',
'Content-Type': 'application/json'
},
body: JSON.stringify(article)
});

Advanced settings include:

  • the interaction with the browser cache
  • the following of redirect responses
  • sending credentials
  • aborting requests

Error Handling

  • The promise returned by fetch does only reject on network errors but not on HTTP errors such as 404 or 500 (in contrast to many AJAX libraries)
  • An accurate error handling should check if the promise was resolved and if its  status is OK
fetch('http://news.org')
  .then(response => {
    if (!response.ok)
      // Error handling based on HTTP status
    return response.json();
  })
  .then(data => {
    ...
  })
  .catch(error => {
    // Error handling (network error, JSON syntax error, etc.)
  });