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