Sam Johnston - Senior Software Engineer

Sam Johnston

Senior Software Engineer
AWS, NodeJS, React

Friday, 2 April 2021

Home » Articles » JavaScript: The Definitive Guide

JavaScript: The Definitive Guide

JavaScript: The Definitive Guide

A collection of useful notes I took whilst reading Javascript: The Definitive Guide, 7th Edition by David Flanagan

Contents

  1. JavaScript Basics
  2. Objects
  3. Arrays
  4. Functions
  5. Classes
  6. Modules
  7. Standard Library
  8. Iterators and Generators
  9. Asynchronous Javascript
  10. Metaprogramming
  11. JavaScript in Web Browsers

JavaScript Basics

JavaScript expressions are "phrases" that will evaluate to a value.

JavaScript statements are "sentences" that end with a semi-colon. Statements are executed and expected to make something happen.

if (expression) {
  statement();
}

First defined operator

First defined operator or nullish coalescing operator (??) is a logical operator that returns its right-hand side operand when its left-hand side operand is null or undefined, and otherwise returns its left-hand side operand.

// This is no good if zero is a valid value, as it's falsy
let max = maxWidth || m.width || 500;

// Switching to the first defined operator solves this issue
let max = maxWidth ?? m.width ?? 500;

globalThis

ES2020 introduces globalThis to replace window and global.

Assignment with Operation

let total = total + salesTax;
let total += salesTax;

Short Circuiting

Javascript expressions are evaluated from left to right and always products a result, or return something.

function saySomething() {
  let greeting = 'hello';
  let returnValue = '_world';

  return greeting && returnValue; // '_world'
  return greeting + returnValue; // 'hello_world'
}

Eval

Always be deeply concerned about as eval statement. It's a security risk/ Eval accepts a string of text that is valid JS, will be executed.

Debugger

Use the debugger statement to halt execution. You can use the dev tools to work out where a problem is in the call stack. For example:

function myBrokenFunc(value) {
  if (value === undefined) {
    debugger();
  }

  return value;
}

Dev tools will halt execution exactly when myBrokenFunc is invoked with no value, allowing you to trace the calls in the call stack and fix.

Typeof Operator

Typeof is only useful when you need to distinguish between an object and a primitive type:

if (typeof value === 'string') {
  console.log(string);
}

InstanceOf Operator

Evaluates to true if the left side object is an instance of the right side class.

const today = new Date();
if (today instanceof Date) {
  console.log(hello);
}

Switch Statement

Use a switch statement when all conditional branches depend on the value of the same expression:

if (number === 1) {
  console.log('one');
} else if (number === 2) {
  console.log('two');
} else {
  console.log('gimme a number');
}

// VS

switch (number) {
  case 1:
    console.log('one');
  case 2:
    console.log('two');
  default:
    console.log('gimme a number');
}

If Else

FUN FACT: Is not a fully qualified statement like elsif in PHP. It's two statements combined.

Hoisting

Hoisting is when a variable declaration is lifted/moved to the top of the enclosing function / execution context this means you can use it before it is defined.

Symbols

A symbol is a symbolic reference to a value. It's always unique but will refer to it's initial value. Very useful for extending a third party object and you want to bve sure nothing is overwritten.

Enumerable

An enumerable property simple means that it can be viewed when it its iterated upon, for example by using Object.keys(obj)

If a property is created by means of assignment, it is enumerable by default. To hide a property and prevent it being iterable you must use Object.defineProperty(...)

Object.defineProperty(myObj, 'new-property', {
  value: 'am new',
  enumerable: false,
  writeable: false,
  configurable: false,
});

Objects

Cloning Objects

// Overwrites everything in obj with defaults
Object.assign(obj, defaults);

// Creates a new object, copies the defaults first, the overwrites with overrides
const newObj = object.assign({}, defaults, overrides);

// Prefer spread operator
const newObj = { ...defaults, ...overrides };

Underscores

Underscores that prefix a property name in an object generally hint that the property is for internal use only.

Prototypal Inheritance

Simple means that properties of an object are inherited from its parent.

Some methods on the Object.prototype are intended to be replaced:

let myObj = {
  x: 1,
  y: 2,
  toString: () => 'am string that represents the object - i need to be defined',
};

myObj.toString(); // am string that represents the object - i need to be defined

// Otherwise this would simply be [Object, object]

Getters and Setters

Getters and setters are property accessors as opposed to data properties. They can be used to create a robust, reliable interface to data properties.

let myObj = {
  name: 'Sam',
  get name() {
    return this.name.toLowercase();
  },
  set name(name) {
    thi.name = name;
  },
};

If a data property has both a getter and setter it can be considered a read/write property.

If a data property has only a getter it can be considered a read only property.

If a data property has only a setter it can be considered a write only property, ideally this property should return undefined.

Arrays

Array Iterator Methods

ForEach()

ForEach() passes each element of your array to the function you provided and can do something with each element, it mutates the original array if you configure it do so.

map()

Map() passes each element of your array to the function you provided and returns a new array of any return values from that function. (ie: it creates a mapping from the provided array)

filter()

Filter() returns an array containing a subset of values from the original array. The function you provide is a predicate function - one that returns true or false given the condition.

let array = [1, 2, 3, 4, 5];
array.filter((element) => element < 3); //returns [1,2]

every() and some()

These are predicate functions in their own right. They return true if every, or some of the elements match your provided function.

reduce() && reduceRight()

Reduce() accepts two arguments, the first is the actual reducer function, the second is an option initial value. The reducer function aims to "reduce" the provided array by accumulating a value based from the current iterated value.

concat()

Concat() concatenates arrays and flattens them one level down. It will return the resulting array.

let array = [1, 2, 3];
array.concat([4, 5, 6]); // returns [1,2,3,4,5,6]

indexOf()

IndexOf() searches an array for a given value and then returns that values index, if it exists. If the value can not be found it returns -1.

includes()

Includes() accepts a single argument and returns true if it can be found in the array, or otherwise false.

sort()

Sort() by defaults sorts an array in ascending order, numbers first, followed by alphabetical (case sensitive) values. Or you can provide your own sorting algorithm. The function you provide will return a number. If the number is negative it is sorted to the beginning, or if it is greater than zero, it is sorted to the end. If the number is zero, this position stays the same for that element.

Array Prototype Methods

Array.of()

Allows for the creation of arrays with a single element

let newArray = Array.of(10); // [10]

// VS

let newArray = new Array(10); // []

Array.from()

Array.from() is useful for when working with array like responses (objects with a length property, Maps, Sets WeakMap WeakSet etc)

Array.from() accepts a optional argument which is a function, similar to mpa() but using this is more efficient, because the array has not yet been created.

Array.from(['a','b','c']), (element) => element.toUpperCase()) // ['A','B','C']

Iterating Arrays

In ES6 there are two main ways to iterate through an array; forEach and for/of loops

The forEach() array method is preferred for functional programming applications, otherwise feel free to use the for/of loop.

let string = 'hello world';
for (let char of string) {
  console.log(char);
}

for (let [index, char] of string.entries()) {
  console.log(char);
  console.log(index);
}

Functions

Function declarations

Functions declared in a block of javascript code are available throughout that block. They will be defined before the interpretor executes any code in the block.

ie: They are hoisted to the top of the block.

Function expressions

It is up to you to assign a function expression to a variable, uf you are going to need to call it again. Or alternatively you can name the function so it can be used recursively.

Function expressions are not hoisted, if one is assigned to a variable it is not ready to be used until that express has been evaluated.

// Function expression
function square(x) {
  return x * x;
}

// named function expression
const f = function factorial(x) {
  if (x <= 1) {
    return 1;
  } else {
    return x * factorial(x - 1);
  }
};

// Anonymous function expression
[3, 2, 1].sort((a, b) => a - b); // [1,2,3]

// IIFE
(function (x) {
  return x * x;
})(11);

// Arrow function
const myFunc = (someArg) => someArg;

Arrow Functions

Do not have their own invocation context. They inherit this from the environment in which they are defined.

They also do not inherit a prototype property so can not be used as constructors

Recursive Functions and The Call Stack

When a function calls another function a new execution context is added to the stack. When execution is complete, the context is removed from the stack and returns to the previous function, until all executions have completed. It's important to consider memory when working with recursive functions that call themselves. Too many stacks can result in a "maximum call stack size exceeded" error

Indirect Invocations

Javascript functions are objects, and like all objects they have their own methods.

.call() and .apply() invoke the function indirectly. Both methods accept two arguments; the first, a value for this which is used as the execution context, and an optional second argument which are the arguments to provide to the called function. .apply() requires this to be an array of arguments, but .call() simply accepts n* arguments.

Functions as Namespaces

It's sometimes useful to define a function to simply act as a temporary namespace where you can set variables without cluttering the global namespace

Closures

Remember the fundamental rule of lexical scoping: JavaScript functions are executed using the scope they were defined in

Technically, all javascript functions are closures but because most functions are executed from the same scope they were defined in, it doesn't normally matter that a closure was involved.

Every closure has three scopes:

  • The local scope (own scope)
  • The outer function scope
  • The global scope

Closures are useful because the let you associate data (the lexical environment => variables), with a function that operates on that data.

function makeBig(size) {
  // returning a function is key to closures so it can later be invoked from another scope
  return function () {
    // this is a closure because we always have access to fontSize if we assign the function to a variable
    document.body.style.fontSize = size + 'px';
  };
}

var makeTen = makeBig(10);
var makeTwenty = makeBig(20);

makeTen();

Function Properties, Methods and Constructors

The length property

Specifies the "arity" of a function; the number of parameters it declares in its parameter list, which is usually the number of arguments the function expects.

Note: ...rest is excluded from the length and not counted.

call() and apply()

These function prototype methods allow you to "call through" another object, temporarily changing the this keyword for that invocation.

After the "this" parameter, .call() accepts an unlimited number of additional parameters which are used as the arguments to the function you are invoking.

.apply(), acts the same as .call() but instead accepts an array of arguments as its second parameter.

.bind() is similar to the above but does not call the function. It allows you to save the bound function to a variable so it can be executed at a later time, all supplied arguments are also bound to the saved function, so can me omitted when the method is to be invoked.

var savedFunction = makeBig.bind(this, 10);
savedFunction();

Higher Order Functions

Any function that accepts a function as an argument (technically a callback if its invoked) or returns another function can be known as a higher order function.

The help to ensure functions are generalized and that your code is DRY.

Recursion

A textbook example of recursion is progressing down through a file tree of multiple levels.

Put simply thought, it is a function that calls itself as many times as it needs to until a condition is met.

The condition is called a base condition, once it is met the function finally returns

Recursion can be used to transform nested objects into arrays, or to create tree structures from otherwise flat data

// A trivial recursive function...
const countDownFrom = (number) => {
  if (number === 0) return;
  console.log(number);
  countDownFrom(number - 1);
};

Classes

In JavaScript classes use prototype based inheritance and we can say that if two or more objects inherit methods or values (state, props) from the same prototype, then we can say that they are instances of the same class.

Constructor functions define, in a sense, classes, and classes have names by convention that begin with capital letters.

Pre ES6 - Constructor Functions

function SayHello(name, greeting) {
  this.name = name;
  this.greeting = greeting;
}

// Typically you do not want to do this, as it overwrites the whole prototype object, normally you want to set a key explicitly
// ie: SayHello.prototype.printMessage = {...}
SayHello.prototype = {
  printMessage: function () {
    return `Hi ${this.name}, ${this.greeting}`;
  },
};

let Sam = new SayHello('sam', 'how are you?');
Sam.name; // sam
Sam.printMessage(); // Hi sam, how are you?

ES6 Classes

Are fundamentally the same as ES5 function constructors the are really just syntactic sugar.

class SayHello {
  constructor(name, greeting) {
    this.name = name;
    this.greeting = greeting;
  }

  printMessage = function () {
    return `Hi ${this.name}, ${this.greeting}`;
  };
}

let Sam = new SayHello('sam', 'how are you?');
Sam.name; // sam
Sam.printMessage(); // Hi sam, how are you?

All code within the body of a class declaration is implicitly ruled by strict mode.

Class declarations are not hoisted. You cannot instantiate a class before you declare it.

Static Methods

You can define a static method on a class by prefixing the method with the static keyword.

Static methods are defined as properties of the constructor not the prototype.

Static methods should avoid using this.

class ClassStub {
  static parse(string) {
    return string;
  }
}

//  Static methods are called directly on the class whereas normal class methods are called from the instantiated version of a class.
// ie: they don't need the new keyword
let string = ClassStub.parse('hello');

Subclasses

In object oriented programming a Class B can extend or "subclass" another Class A. We say that A is the "superclass" and B is the "subclass".

Instances of B inherit all the methods of A, but can be extended or even have specific methods overwritten.

ES6 introduces the extends clause to a class declaration to allow us to simply make a superclass.

If you extend a class, then the constructor for your class must call super() in order to invoke the constructor of the super class.

If your class does not have a constructor then one will be defined automatically, it'll pass all the values to super().

You may not use the this keyword in your constructor, without first calling super(), this enforces a rule whereby superclasses get to initialise themselves before a subclass does.

You can treat super() as an object and call methods on the original superclass, even if the subclass overwrote them. super.set(key, value)

Delegation instead of inheritance

It is often easier and more useful to create a new class that creates instances of other classes, rather than extending them.

This is known as object composition and one should favour composition over inheritance. Inheritance (extending) is often optional.

Modules

Modularity is mostly about encapsulating or hiding private implementation details and keeping the global namespace clean so that modules can not accidentally modify the variables, functions and class defined by other modules.

It also helps code management and allows for clear, concise and distinct code organisation.

ES5 and before had no build in module support so developers relied on classes, objects and closures to do this.

Methods on a object are scoped to that object and so do not pollute the global namespace.

By using an IIFE you can hide away private implementation details and store the return value inside a variable, this will ensure the whole function gets run, but only the return values serves as the public API.

NodeJS

In NodeJS each file is an independent module and the constants, functions contained within are private to that file, unless explicitly exported.

Node modules import other modules with the require function and export and public api using module.exports.

const myFunction = () => 42;

module.exports = myFunction;

ES6 Modules

These use default and named exports and can by used by using the keywords import and export.

// default import
import something from './somewhere';
// named import
import { somethingElse } from './somewhere';

//named export
export const myNamedExport = () => 42;

//default export
export default myDefaultExport;

We can also rename exports when importing them if their namespaces clash or we want to use something nicer

import { default as prettyName } from './somewhere';
import { fuglyFunction as wellHelloThere } from './somewhere';

We can also import all name exports at once with a wildcard. this will ass them all to a new namespace.

import * as something from './something';
something.myExportedMethod();
something.else();
something.etc();

Re exporting

By re exporting symbols we give the consumer more control on what the are importing.

//someFile.js
export { mean } from './stats/mean';
export { standardDeviation } from './stats/dev';

import { mean, standardDeviation } from 'someFile.js';

Dynamic imports

You can dynamically import files/modules using the import function

async function analyzeData(data) {
  let statsModule = await import('./path/to/module');

  return {
    average: statsModule(data),
  };
}

const result = await analyzeData(data);

The JavaScript Standard Library

The following subsections make up what can be considered the standard library of javascript

The Set Class

A Set is a collection of values, similar to an Array however a set is not ordered or indexed and duplicated values are not allowed. Any given value is either a member of a Set, or not a member.

It is not possible to ask how many times a value appears in a set.

Create a Set object by calling the constructor with any iterable object.

let mySet = new Set('sam'); // new Set, 3 elements "s", "a", "m"
mySet.size; // 3 - similar to .length on Arrays

Set membership is based on strict equality === so a set can contain both 1 and "1".

After a set has been initialised you can use three methods ot manage the membership of it. add(), clear() and delete()

mySet.add('johnston');

mySet.delete('s');

mySet.clear();

Sets are optimised for membership testing. it is better to use the mySet.has() method over the includes() of the Array type.

In practise you'll only want to use Sets for membership testing, and you should always use .has().

Sets are iterable however, so if you need to work with one after the fact you can convert them.

const myArray = [...mySet];

Sets do not have indexes but you can rely on the order of a set to be the order in which you added elements to it.

// forEach method of a set, only allows one arg, index does not exist
mySet.forEach((element) => console.log(element));

The Map Class

A map objects a set of values, known as keys, where each key has another value associated to it, or "mapped" to it.

Similar to Arrays, but instead of sequential integers as keys, Array[3], maps allow for arbitrary values that can be used as indexes.

Similar to Set's we have the set(), get(), has(), delete() and clear() methods available on any given Map.

If set()is called with a key that already exists, it's corresponding value will be updated.

You can rely on the order of a Map to be in the order in which you added elements to it (same as a Set).

You can easily destructure a Map to get all the keys and values out:

const myMap = new Map([
  ['key', 'value'],
  [1, 2],
]);
const all = [...myMap]; // [["key", "value"], [1, 2]]
const keys = [...myMap.keys()]; // ["key", 1]
const values = [...myMap.values()]; // ["value", 2]

WeakMap and WeakSet

These are variants of Map and Set that allow for their values to be garbage collected. If values inside a WeakMap or WeakSet, are no longer reachable or have no external references they will be selected for garbage collection.

The keys for WeakMap, and the value for a WeakSet must be objects or Arrays. Primitive values are not subject to garbage collection

Regular Expressions

A regular expression is an object that describes a textual pattern.

let literalPattern = /s$/;
let constructorPattern = new RegExp("s$);
  • [] A character group.
  • {} A repetition group.
  • [...] Any one character between the bracket group.
  • [^...] Any one character NOT between the bracket group.
  • . Any chracter except a new line terminator.
  • \w Any ASCI word character [a-zA-Z0-9_].
  • \s Any unicode whitespace character.
  • \d Any ASCI digit character [0-9].

Repetition Groups

  • {n, m} Match the previous item at least n times, but no more than m times.
  • {n, } Match the previous item as least n times or more.
  • {n} Match the previous item exactly n times.
const digitsRegex = new RegExp('d{2,4}'); // match numbers with at least 2 and at most 4 digits
const threeLetters = new RegExp('s{3}'); // match three letter words

Operators

  • | Logical OR statement ("hello"|"hi").
  • (...) Literal capture group ("find me').
  • ^ Denotes the start of a regular expression string (^\w{3}) // 123, 123456.
  • $ Denotes the end of a regular expression string (^\w{3}$) // 123

Flags

  • g Global, find all matches within a string.
  • i Case Insensitive, matching will be case-insensitive.
  • m Multiline, matching with be multiline.

String Methods for Pattern Matching

search() Accepts a regular expression argument and returns either the position of the first matching string or -1 if one cannot be found

const regex = /script/i;
'JavaScript'.search(regex); // 4
'Python'.search(regex); // -1

replace() Accepts two arguments, the first being a regular expression to search for (or just a plain string), the second being the value to replace it with.

const string = 'The quick brown fox';
string.replace(/\b\w{3}\b/g, ''); // The fox

match() Is the most useful and returns an array of all the matches found or null.

const string = 'The quick brown fox';
string.match(/\b\w{3}\b/g, ''); // ["The", "fox"]

test() Is the simplest way to use regular expressions, it returns true or false

const pattern = /\w{3}/g;
pattern.test('123'); // true

Dates and Times

const currentTime = new Date(); // Returns the current date and time
const epochTime = new Date(0); // Returns the number of milliseconds since the 1970 epoch

One quirk of the Date API is that the first month of a year is 0 but the first day of the month is 1

Dates are always in the local time of the machine (server or client), if you want a UTC (GMT) you can use:

const utcTime = new Date(Date.UTC());

If you console.log() a date, it will also by default be in the local time, to log it in UTC, you must explicitly convert it to a string with toUTCString(), or toISOString().

let now = new Date(); // Tue Jul 04 2021 15:09:34 GMT+0100 (British Summer Time)
now.getFullYear(); // 2021
now.getMonth(); // 6
now.getDate(); // 4
now.getHours(); // 15
now.getMinutes(); // 9
now.getSeconds(); // 34
now.getMilliseconds(); // 871
now.getUTCFullYear(); // 2021 (GMT)

now.setFullYear(2000); // Tue Jul 04 2000 15:09:34 GMT+0100 (British Summer Time)
now.setMonth(0); // Wed Jan 06 2021 15:09:34 GMT+0000 (Greenwich Mean Time)
now.setDate(10); // Sun Jan 10 2021 15:09:34 GMT+0000 (Greenwich Mean Time)
now.setHours(now.getHours() + 1); // Tue Jan 04 2000 16:09:34 GMT+0100 (British Summer Time)
now.setMinutes(now.getMinutes() + 30); // Tue Jan 04 2000 16:39:34 GMT+0100 (British Summer Time)
now.setSeconds(now.getSeconds() + 30); // Tue Jan 04 2000 16:40:04 GMT+0100 (British Summer Time)
now.setMilliseconds(1000); // Tue Jan 04 2000 15:09:35 GMT+0100 (British Summer Time)
now.setUTCFullYear(2021); // Tue Jul 06 2021 15:09:35 GMT+0100 (British Summer Time)

Timestamps

Internally dates are stored as integers that specify the number of milliseconds since midnight January 1 1970 UTC (The Unix epoch === 0, any time before this, is a negative integer). To get the internally stored date you can use now.getTime() or Date.now()

Date Arithmetic

Using timestamps works well for adjusting a date in seconds but you can also use the setDate() methods listed above to programmatically increase/decrease a date by a specified time frame. This works even if the the date overflows, for example if you add 16 months to a date, it will be incremented by 1 year and 4 months.

let now = new Date(); // Tue Jul 04 2021 15:09:34 GMT+0100 (British Summer Time)
now.setHours(now.getHours() + 1); // Tue Jan 04 2000 16:09:34 GMT+0100 (British Summer Time)
now.setMinutes(now.getMinutes() + 30); // Tue Jan 04 2000 16:39:34 GMT+0100 (British Summer Time)
now.setSeconds(now.getSeconds() + 30); // Tue Jan 04 2000 16:40:04 GMT+0100 (British Summer Time)

Formatting and Parsing Date Strings

It's best to avoid showing these dates to end users. If you need something quick and dirty you can use the below, but you should prefer the Intl.DateTimeFormat() class.

let now = new Date();
now.toString(); // "Tue Jul 06 2021 15:27:30 GMT+0100 (British Summer Time)"
now.toUTCString(); // "Tue Jul 06 2021 15:27:30 GMT+0100 (British Summer Time)"

Error Classes

The standard Error class has three properties:

name: "Error" || "EvalError" || "RangeError" || "SyntaxError" etc message: The value you passed to the error constructor toString(): returns "EvalError: ${message}"

There are built in subclasses for Errors which include:

"EvalError" || "RangeError" || "ReferenceError || "SyntaxError" || "TypeError" || "URIError"

But you should feel free to extend off Error and create your own subclass if you'd like something more specific.

class HTTPError extends Error {
  constructor(status, statusText, url) {
    super(`${status} ${statusText}: ${url}`); // call the super class with the message.value
    this.status = status;
    this.statusText = statusText;
    this.url = url;
  }

  get Name() {
    return 'HTTPError'; //Overwrite the name property of the superclass
  }
}

const notFoundError = HTTPError(
  404,
  'Not Found',
  'https://samjohnston.co.uk/404'
);

notFoundError.status; // 404
notFoundError.message; // 404 Not Found: https://samjohnston.co.uk/404
notFoundError.name; // "HTTPError"

JSON Serialisation and Parsing

JSON = JavaScript Object Notation

The process of converting data structures into streams of bytes or characters is known as serialisation. It is commonly needed when saving data or transferring data over the network.

JSON.stringify(data) Takes a data structure and converts it into a string

If your intention is console.log the result, you can provide an optional third argument:

JSON.stringify(data, null, 2) // indent the resulting object with 2 spaces per property.

JSON.parse(data) Takes previously stringified data structure and restores it to it's initial form

If the previously stringified data contains something that is not natively supported, OR instead implements its own toJSON() method, the resulting form will not match the original form. To solve this you can pass a method as the optional second parameter for both JSON.parse and JSON.stringify.

If you pass a function to JSON.parse() second argument - it is known as a reviver function.

If you pass a function to JSON.stringify() second argument - it is known as a replacer function.

// Example Reviver Function

let data = JSON.parse(text, function (key, value) {
  if (key[0] === '_') {
    return undefined; // remove any values whose property starts with "_"
  }

  if (typeof value === 'string' && isDateRegex.test(value)) {
    return new Date(value); // Return a new date object if the value is a valid ISO date string.
  }

  return value; // otherwise just return the value
});
// Example Replacer Function

// JSON.stringify either accepts an array or a replacer function as its optional second argument

let data = JSON.stringify(addressData, ['city', 'state']); // only returns the serialised city and state properties and values.

let data = JSON.stringify(addressData, function (key, value) {
  if (value instanceof RegExp) {
    return undefined; // if property value is a regular expression, return undefined
  }

  return value; // otherwise return the value
});

The Internationalisation API

Intl MDN Docs

These methods are well supported by browsers but not yet part of the ECMAScript standard, so many need to be polyfilled. Node also supports it but you will need additional packages to add the localisation data needed. Out of the box Node defaults to US localisation.

Formatting Numbers

Once you have created an Intl.NumberFormat object you can use it by passing a number to it's format() method:

let euros = Intl.NumberFormat('es', { style: currency, currency: 'EUR' });
euros.format(10); // "10,00 €" - Spanish currency formatting.

A useful feature is to bind the format() method to a variable:

const data = [0.05, 0.75, 1];
const formatData = Intl.NumberFormat(undefined, {
  style: 'percent',
  minimumFractionDigits: 1,
  maximumFractionDigits: 1,
}).format();

data.map(formatData); // ["5.0%", "75.0%", "100%"]
Useful Properties
const properties = {
  // "decimal" || "percent" || "currency"
  style: 'decimal',
  // ISO country code. Required if style === currency
  currency: 'EUR',
  // "narrowSymbol" || "symbol" || "code" || "name"
  currencyDisplay: 'narrowSymbol',
};
Formatting Dates and Times

Works similar to Intl.NumberFormat, but you should only specify properties in the options that refer to fields that you want in your output.

const today = new Date();

// With no parameters you get a basic numeric based date
Intl.DateTimeFormat('en-US').format(date); // 1/2/2020
Intl.DateTimeFormat('en-GB').format(date); // 2/1/2020

// Text based dates
Intl.DateTimeFormat('en-GB', {
  weekday: 'long',
  month: 'long',
  year: 'numeric',
  day: 'numeric',
}).format(date); // Thursday, January 2nd, 2020
property values
weekday long (Monday) - short (Mon) - narrow (M)
year numeric (2021) - 2-digit (21)
month numeric (2) - 2-digit (02) - long (March) - short (Mar) - narrow (M)
day numeric (2) - 2-digit (02)
hour numeric (2) - 2-digit (02)
minute numeric (2) - 2-digit (02)
second numeric (2) - 2-digit (02)
timeZoneName long (British Summer Time) - short (GMT+1)
Console API

console.table() is useful for rendering a short array of objects or JSON. Each of the objects must have the same properties. In this case, each object is formatted as a row, and each property is a column.

You can pass an optional second argument, and array of property names, to explicitly set the columns you're interested in.

console.log([obj1, obj2, obj3], ['name', 'age']);
Formatting output with console

You can interpolate the text output of a log with the following special characters

flag description
%s is substituted with the provided string
%i and %d is converted to a number and truncated to an integer
%f is substituted for a number
%o and %O is substituted for an object
%c is substituted for a string of CSS
// CSS Substitution
console.log('%c I AM A BLUE LOG', 'color: blue');

const arrayOfNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Interpolate each number and output as a blue log
arrayOfNumbers.forEach((number) => {
  console.log('%c I AM THE NUMBER %s', 'color: blue', number);
});
URL API

You should not use the deprecated escape and unescape methods. And you should avoid using encodeURI and encodeURIComponent as well as their decode counterparts if you can. They all use a single encoding scheme for the whole URL, when different parts of the URL should be treated differently. The URL class fixes this.

const url = new URL('https://samjohnston.co.uk');
url.pathName = '/blog';

const output = {
  hash: '',
  host: 'samjohnston.co.uk',
  hostname: 'samjohnston.co.uk',
  href: 'https://samjohnston.co.uk/blog',
  origin: 'https://samjohnston.co.uk',
  password: '',
  pathname: '/blog',
  port: '',
  protocol: 'https:',
  search: '',
  searchParams: URLSearchParams,
  username: '',
};

Some protocols like FTP have additional user and password properties.

const url = new URL('https://samjohnston.co.uk');
const searchParams = new URLSearchParams();

searchParams.append('term', 'search string');
url.search = searchParams;

console.log(url.href); // https://samjohnston.co.uk/?term=search+string

If the URL constructor is instantiated with a url containing a query string, you can access the internal URLSearchParams class directly with url.searchParams

Iterators and Generators

Iterators - Symbol.iterator

Iterable objects like Arrays, Sets, Maps and Strings can be iterated over. They can also be spread or destructured.

You can make your own type iterable by adding a [Symbol.iterator]() method to your class. This method must return an object that has a next() method, and this must return a result object containing either a value or a done property.

/*
 * A Range object represents a Range of numbers { x: from <= x <= to}
 * Range defines a has() method for testing whether a given number is a member of the Range.
 * Range is iterable and iterates all integers within the Range.
 */
class Range {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  // Make a Range act like a Set of numbers
  has(x) {
    return typeof x === 'number' && this.from <= x && x <= this.to;
  }

  [Symbol.iterator]() {
    // Each iterator instance must iterate the range independently of others. So we need a state
    // variable to track our location in the iteration. We start at the first integer => from
    let next = Math.ceil(this.from); // This is the next value we return
    let last = this.to; // We won't return anything if > this

    // This is the iterator object
    return {
      // This next() method is what makes this an iterator object
      next() {
        // It must return an iterator result object
        return next <= last ? { value: next++ } : { done: true };
        // If we haven't returned the last value yet, return the next value and increment it. otherwise indicate that we are done.
      },
    };
  }
}

for (let x of new Range(1, 10)) {
  console.log(x); // Logs numbers 1, 10
}

[...new Range(-2, 2)]; // => [-2, -1, 0, 1, 2]

The for/of loop and the spread operator can be used with iterable objects. An object is iterable if it has a method with the symbolic name [Symbol.iterator]() which returns an iterator object. An iterator object has a next() method that returns iteration results. An iteration result object has a value property that holds the next iterated value, if there is one. If the iteration has completed, then the result object must have a done property set to true.

You can implement your own iterable objects by defining a [Symbol.iterator]() method that returns an object with a next() method which itself returns an iteration result object. You can also implement functions that accept iterator arguments and return iterator values.

Generators

A generator is kind of iterator. It's particularity useful when the values to be iterated are not the elements of a data structure, but instead are the results of a computation.

When you invoke a function generator, a generator object is returned. The function itself does not run!

function* oneDigitPrimes() {
  yield 2;
  yield 3;
  yield 5;
}

const primes = oneDigitPrimes();

primes.next().value; // => 2
primes.next().value; // => 3
primes.next().done; // => false
primes.next().value; // => 5
primes.next(); // => { value: undefined, done: true }

A generator function will automatically set done to true, when there is nothing else to yield.

It's best to not abuse generators, there is probably a more simplistic approach.

Generators that need to "clean-up" can do so in the return statement, or within a try/finally block. For example, when working with the file system (closing files when complete).

Generator functions (functions defined with a asterisk function*) are another way to define iterators. When you invoke a generator function, the body of the function does not run right away, instead, the return value is an iterable iterator object. Each time the next() method of the iterator is called, another chunk of the generator function runs.

Generators can use the yield operator to specify the values that are returned by the iterator. Each call to next() causes the generator function to run up to the next yield expression. The value of that yield expression then becomes the value retuned by the iterator. When there are no more yield expressions, then the generator returns and the iteration is complete.

Asynchronous Javascript

Some computer programs, such a scientific simulations, are compute bound - they run continuously until they have computed the result synchronously. Most real world computer programs are significantly asynchronous. They need to stop computing to wait for data to arrive, or an event to occur. JavaScript is typically event driven. Generally, JavaScript programs await for user input in the browser, and client requests in Node.

JavaScript provides mechanisms for working with asynchronous code. (Promises/async/await) , but fundamentally it is a synchronous programming language.

At its most fundamental level, asynchronous programming in JavaScript is done with "callbacks".

Client Side Events

// call the callback function, once, after a set interval in milliseconds.
setTimeout(callbackFunction, interval);

// Call the callback function repeatedly every "interval"
const intervalId = setInterval(callbackFunction, interval);

// setInterval returns an id, which can be used to clear the interval
function stopChecking() {
  clearInterval(intervalId);
}

// Event listeners can be applied to DOM nodes or globally (window), the first argument relates to the event type
// and the second is the callback to invoke when said event occurs
addEventListener('click', callbackFunction);

Node Events

Node is deeply asynchronous and typically uses two parameter callback functions for it's events.

The first holds the errors for when it fails and the seconds holds the successful response.

Node typically registers events using an on() method (instead of the addEventListener() method used in browsers)

// Read a file and output its content
fs.readFile('my-file.txt', 'utf-8', (error, content) => {
  if (error) {
    throw new Error(error);
  }

  console.log(content);
});

request.on('response', (response) => {
  console.log(response);
});

// GET request for Express
expressServer.get('*', (req, res) => handle(req, res));

Promises

A promise is an object that represents the result of an asynchronous operation/computation. That result may or may not be ready yet, there is no way to synchronously get the value of a promise you can only as it to call the callback when a value is ready (resolves // rejects).

Promises must always explicitly return something. Leaving a function to return undefined will always cause issues.

When discussing Promises the correct terminology to use, is as follows:

term description
fulfilled The promise resolved successfully and called the first callback (success)
rejected The promise did not call th second callback (error)
pending Neither callback has returned a result yet
settled Either callback returned a result

A callback will never change its value once its returned.

const result = getJSON.then(handleResponse, handleError);

// OR

const result = getJSON
  .then((response) => {
    // ...
  })
  .catch((error) => {
    // ...
  });

// OR

const getData = async () => {
  try {
    const data = await getJSON();
    return data;
  } catch (error) {
    throw new Error(error);
  }
};
const result = await getData();

Resolved Status

Resolved means that the Promise has become associated with another Promise. The ultimate value of the original Promise is now in the hands of the other Promise. So we can say that the original Promise is "resolved" there is nothing else it can do except wait for the sequential promises to fulfil or reject.

Error Handling

Similar to try/catch/finally promises also have .catch() and finally() method. finally() is called if a promise fulfils or rejects so is a great option for running clean up code (closing files / connections / deleting storage etc).

catch() can be used anywhere inside a promise chain so can actually help with recoverable errors.

// This defensive code example can help recover from random network errors.
// If the first catch throws, then the last catch will be invoked to save the day
queryDatabase()
  .catch((error) => wait(500).then(queryDatabase))
  .then(displayData)
  .catch(displayError);

Promises in Parallel

Promise.all() Either fulfils with a returned promise containing all the provided promises, or rejects immediately after the first failure.

Promise.allSettled() Never rejects the returned Promise and does not fulfil that promise until all input Promises have fulfilled. Resolved to an array of objects containing each promise status (fulfilled/ rejected and it's resolved values.

Promise.race() Given an array of Promises, it will return the result of the promise that returned first ( even if it was rejected).

Metaprogramming

Definition: "Writing code to manipulate other code."

Useful if writing reusable libraries or changing how objects behave.

Property Attributes

As well as having a value, object properties can also have "attributes".

attribute description
writeable Specifies whether the property can be changed
enumerable Specifies whether the property can be iterated
configurable Specifies whether the property can be deleted AND whether the other attributes can be changed

These attributes are useful to library authors because it allows them to "lock down" objects, similar to native JS objects.

You can query an objects property with getOwnPropertyDescriptor()

// If the property is a data property
const hello = { message: 'hello' };
Object.getOwnPropertyDescriptor(hello, 'message');
// => { value: "hello", writable: true, enumerable: true, configurable: true }

// If the property is a data accessor
const random = {
  value: 'hello',
  get random() {
    return this.value;
  },
};

Object.getOwnPropertyDescriptor(random, 'random');
// => { get: random(), set: undefined, configurable: true, enumerable: true }

To set attributes of a property or to create a new property with attributes, you can use Object.defineProperty()

If you want to define multiple properties at once, you can instead use Object.defineProperties() whereby the second argument is an object containing all the properties and the associated attributes.

const myObject = {};

// Add a single property
Object.defineProperty(myObject, 'newProperty', {
  value: 'Am a new property',
  writeable: false,
  enumerable: false,
  configurable: false,
});


// Add multiple properties
const newProperties: {
  anotherProperty: {
    value: "another new property",
    writable: true,
    enumerable: false,
  },
  thirdProperty: {
    value: "am third property",
    writable: true,
    enumerable: false,
  }
}

Object.defineProperties(myObject, newProperties)

Object Extensibility

You can prevent properties being added to an object with Object.preventExtensions().

const myObject = {};
Object.isExtensible(myObject); // => true
Object.preventExtensions(myObject); // This prevents any new properties from being added
Object.isExtensible(myObject); // => false

const myObject = {};
Object.seal(myObject); // Prevents new properties from being added AND makes existing own properties non-configurable.

Object.freeze(myObject); // Prevents new properties from being added AND makes existing own properties non-configurable AND makes all data properties read only

Template Tags

Strings with backticks are known as "template-literals". When an expression whose value is a function is followed by a template literal, it becomes a function invocation. We call this a "tagged template literal". Defining one can be though of as adding a new syntax to JavaScript. Similar GraphQL and Styled-Components.

const html = (strings, ...values) => {
  const escapedValues = values.map((value) => String(value).replace('+', '&'));

  let result = strings[0];
  for (let i = 0; i <= escapedValues.length; i++) {
    result += escapedValues[i] + strings[i];
  }

  return result;
};

let game = ['D+D', 'World of Warcraft'];

html`<div>${game}</div>`; // => <div>D&D, World of Warcraft</div>

JavaScript in Web Browsers

Web browsers display HTML documents. If you want a browser to execute JavaScript from a HTML document you must use a script tag.

<!DOCTYPE html>
<html>
  <head>
    <title>Digital Clock</title>
  </head>
  <body>
    <h1>Digital Clock</h1>
    <span id="clock"></span>
    <script>
      function displayTime() {
        const clock = document.querySelector('#clock');
        const timeNow = new Date();
        clock.textContent = timeNow.toLocaleTimeString();
      }

      displayTime();
      setInterval(displayTime, 1000);
    </script>
  </body>
</html>

By referencing a JS file within a src attribute you can load and execute the JS as if it appeared inline with a script tag. Using external javascript has a number of benefits.

  • Browser Caching - If multiple HTML pages share the same JS, then it will only be downloaded once, subsequent pages will use a cached version.
  • Separation of Concerns - The HTML document houses the structure of the page, a stylesheet houses it's look and an external JS file houses the pages logic.
  • Maintainability - By referencing a single external JS file, in multiple HTML pages you maintain a source of truth, if something needs updating you only need to update one file.

ES Modules

If you have written your JS using modules (import and export) and not used a bundler, then you must load your top level JS file using a script tag that has the type="module" attribute. This will direct the browser to download all the references internal imports recursively.

When Scripts Run - async defer

By default inline and external JS runs synchronously or "blocking" because of the largely miss used document.write() method originally used to output HTML content whilst the page was being parsed. You can opt out of this bad behaviour if your JS does not use document.write() - which it shouldn't

DEFER

Tells the browser to defer execution of a script until it has fully loaded and parsed the page and it is ready to be manipulated.

ASYNC

Runs the script as soon as possible but does not block document parsing while the script is being downloaded.

NOTES

  • Use one directive at a time (async takes precedence if both are present).
  • Deferred scripts run in the order in which they appear in the document.
  • Async scripts run as they load, which means they may execute out of order.
  • type="module" scripts by default execute as if they have the defer attribute.
  • A simple alternative to async/defer is to place your scripts at the end of the HTML file. This way the script can run knowing the HTML page has already been parsed.

ON DEMAND

Sometimes you need to load a script on demand, after tha page has loaded. Like when a user opens a menu or clicks a button. This can be easily achieved with dynamic import() methods, if using modules (with or without a bundler).

Otherwise in vanilla land you can use the DOM apis to add a script tag on demand with an IIFE.

// Asynchronously load and execute a script from a specified URL.
// Returns a Promise that resolves once the script has loaded.
function importScript(url) {
  return new Promise((resolve, reject) => {
    // Create a script element
    const scriptTag = document.createElement('script');

    // Resolve promise when loaded
    scriptTag.onload = () => {
      resolve();
    };

    // Reject Promise if it fails.
    scriptTag.onError = (error) => {
      reject(error);
    };

    // Set the URL attribute
    scriptTag.url = url;

    // Add the script to the document
    document.head.append(scriptTag);
  });
}

The Document Object Model

The DOM API mirrors the tree structure of an HTML document.

The DOM tree borrows terminology from family trees.

term definition
parent The node directly above another node
child The node directly below another node
sibling The node at the same level as another, with the same parent node
descendants The set of nodes at any level below a node
ancestors The set of nodes any level above a node

There is a JavaScript class corresponding to each HTML tag type and each occurrence of a tag is a document is represented by an instance of that Class. Each class has specific properties to relate to the attributes of that HTML tag. For example HTMLImageElement has both src and alt properties. Setting the properties causes the element to render again.

Every run of text content also has a corresponding Text Class.

The Global Object in Web Browsers

There is one global object per browser window or tab. All JS code except that which is running in worker threads share that object.

The global object contains properties for each of the Classes in the standard library (Set, Map, Date, Math, etc), but also has properties that represent the current browser window (innerWidth, history, location etc)

Scripts Share a Namespace

In modules top-level declarations are scoped to the module and must be exported or directly attached to the global/window namespace.

In non module scripts, top level declarations are scoped to the containing document and the declarations are shared between all other scripts running the document.

Older var and function declarations are shared via the global object (window) itself.

Newer const, let and classes are shared and have the same document scope, but do not have properties on any object JavaScript has access too.

Execution of JavaScript Programs

JavaScript can be defined as having two stages of execution.

The first stage, is where the document content has loaded and the code from the script elements is executed.

Afterwards the second stage starts, and is mostly asynchronous and event driven. (based on user input).

The load() or DOMContentLoaded() events can be used to signal that the HTML document is ready to be interacted with JavaScript

JavaScript is single threaded, which means that browsers stop responding to events whilst scripts and event handlers are executing. If the currently executing JS is computationally demanding, the page may load slowly (while it waits for the scripts to run during the first phase of loading). Or the page can appear frozen (while waiting in the second phase - demanding events).

The solution to this is to run demanding tasks in a web-worker to ensure the main thread remains responsive.

JavaScript Web Security

Client-side javascript by default is not able to write, delete or list arbitrary files from the client OS and cannot access the wider network in an unregulated way.

JS same-origin policy

JavaScript may only access documents which live on the same origin, this is particularly important when thinking about iframes.

If a browser loads a page "http://example.com/index.html", which itself contains an iframe pointed at "http://example.com/second.html", JS is able to access both documents as they are on the same origin. If a second iframe tries to load http://external.com it will not have any permission to access that document.

An origin is the same if the protocol, host and port are the same.

# These are all different origins
http://example.com:80
https://example.com:443
http://sub.example/com

JS CORS - cross-origin-resource-sharing

As well as Documents, same-origin policy also applies to HTTP requests.

The same origin policy causes issues for larger sites who have content (microservices/API's) on multiple subdomains. This is where CORS (cross origin resource sharing) comes in handy. CORS extends HTTP with a new Origin: request header and a new new Access-Control-Allow-Origin: response header. It allows the server to explicitly set origins that are allowed and trusted and any browsers that see these headers will respect the setting.

JS XSS - cross-site-scripting

XSS is where an attacker "injects" HTML tags or scripts into a target webpage. Client side JS programmers must be aware of this vulnerability.

A web page is especially vulnerable to XSS if it accepts user input and uses it to dynamically create or update a page or any of its elements, without first sanitising the inputted data.

Events

JavaScript is a dynamic asynchronous event driven language. A web browser generates an event whenever something interesting happens, ( document load, mouseover, network, click, etc). Some basic terminology would be:

Event Type - Specifies the type of event that occurred (mousemove || click || mousedown)

Event Target - The Object on which the event occurred, could be the window, document an Element or a Worker.

Event Hander / Event Listener - A function this is invoked whe the specified event type occurs on the specified event target. When the function is invoked we say that the browser has fired || triggered || dispatched the event.

Event Object - These are passed as arguments to the event handler functions and contain both type and target properties, referring to the Event Type and Event Target. Many Events only have these properties and in those instances, its normally the occurrence of the event that is important rather than in its details. Additional details are available for other Events, such as Event.key for keypress events.

Event Propagation

When certain events occur on Elements of a page they propagate, or "bubble up" the document tree looking for an Event Handler which could be registered on any of the parent DOM nodes all the way up to the document itself.

Useful Event Categories

# Device Dependant Input Events
mousedown
mousemove
mouseup
keydown
keyup
touchstart
touchmove
touchend
# Device Independent Input Events
click
pointerdown
pointermove
pointerup
# User Interface Events
focus
change
submit
blur
# State Change Events
load
DOMChangeEvents
online
offline
popstate
waiting
playing
seeking
volumechange
success
error

Registering Event Handlers

There are two ways to register an Event Handler, the first is old school and uses properties on the target object of document.

// Setting Event Handler Properties

window.onload = function () {
  console.log('loaded');
  init();
};

// Setting Event handler attributes (frowned upon)
<button onclick="console.log('clicked')">Click me</button>;

Worth noting that Event Handler properties name are always in lowercase in vanilla JavaScript, but camelCased in React.

The second method is to use addEventListener() which is a built in method defined on all objects that can be an Event Target, including the window and document.

const button = document.querySelector('.button');
button.addEventListener('click', () => console.log('clicked'));

The Event Type (name) omits on the "on" prefix seen in the first example.

You can set multiple event listeners on the same object and the handlers will be invoked in the order they were registered. Unless the event listeners have the exact same arguments.

Removing Event Handlers

Sometimes events need to be transient and require cleanup. You can invoke the removeEventListener() method with the exact same arguments to stop listening for the choosen event. This is why its often preferable to pass a reference to the handler rather than inlining it.

const handleClick = () => console.log('clicked');
document.addEventListener('click', handleClick);
document.removeEventListener('click', handleClick);

Both addEventListener() and removeEventListener() have a third optional argument fr specific options.

document.addEventListener('click', handleClick, {
  capture: true,
  once: true,
  passive: true,
});

capture - Register the Event Handler as a "capturing" handler.

once - Automatically remove the event listener after it has triggered once.

passive - Indicates that the handler will never call preventDefault, and that the browser can expect to run normally.

Event Handler Invocation

When an Event occurs all the registered Event Handlers will be invoked, in the order they were registered.

Event Handler Arguments

An Event handler has access to an "Event" argument that contains details about the event that occurred. The event typically has the following properties.


type Event {
  type: "The type of Event that occurred",
  target: "The object on which the event occurred",
  currentTarget: "For events that propagate, this property is the object on which the current event handler was registered on",
  timeStamp: "A timestamp in milliseconds for when the event occurred",
  isTrusted: "true if the event was dispatched by the browser itself, false if JS code dispatched it."
}

Specific kinds of events have additional properties like clientX and clientY which specify the window co-ordinates of where the event occurred.

this always refers to the object that the event was registered on (execution context).

Event handlers can return a value, but shouldn't. Use preventDefault() instead.

Event Capturing

When an event target is the Document or an element contained within that document, and a event which is being listened for is dispatched, an event listeners registered on any of the elements parents, all the way up to the document and window itself are invoked, in the order in which they were registered. This is useful because you can register a single listener on a form for a change event and it'll be invoked when any of the child elements dispatch the event. (ie: it bubbles up).

Most events on document elements bubble except focus, blur and scroll events.

If capture: true is set in the options object of a addEventListener declaration, it will in fact be invoked BEFORE the event listeners on the target object. You can think of this as the first stage of event propagation, any capturing handlers will be invoked first, starting with the one highest in the DOM tree, it will then bubble down until the parent of the event target where the second stage of event propagation happens - the actual event handler on the event target triggers and the third stage is where that event proceeds to bubble up.

This is useful for peeking at events before they are delivered to their target. A common usage for this is for handling mouse drags, where mouse motion events need to be handled by the object being dragged and not the elements being dragged over.

Event Cancellation

You can cancel the default behaviour of an event by invoking the preventDefault() method this is useful on link and button elements with event handlers, for scrolling and typing events or in any case where browser automatically respond to user events natively.

You can also cancel the event propagation by invoking stopPropagation() which will prevent the event from bubbling any higher (or lower if used with a capturing event handler).

Finally, if you want to ensure no other handlers registered on an event target are triggered, you can invoke stopImmediatePropagation() and only that handler will be invoked.

Dispatching Custom Events

You can create your own custom events which are useful for heavy network requests or computationally intense tasks.

const isLoading = new CustomEvent('isLoading', { detail: true });
const isNotLoading = new CustomEvent('isLoading', { detail: false });

document.dispatchEvent(
  isLoading,
  fetch(url)
    .then(doSomething)
    .catch(handleError)
    .finally(() => {
      document.dispatchEvent(isNotLoading);
    })
);

document.addEventListener('isLoading', (event) => {
  event.detail ? showLoadingSpinner() : hideLoadingSpinner();
});

Modified: Sunday, 15 August 2021