infuerno.github.io

Haverbeke: Eloquent JavaScript

Introduction

We think we are creating the system for our own purposes. We believe we are making it in our own image… But the computer is not really like us. It is a projection of a very slim part of ourselves: that portion devoted to logic, order, rule, and clarity. — Ellen Ullman, Close to the Machine: Technophilia and its Discontents

When action grows unprofitable, gather information. When information grows unprofitable, sleep. — Ursula K. Le Guin, The Left Hand of Darkness

Programming, it turns out, is hard. You’re buidling your own maze, in a way, and you might just get lost in it. Without care, a programs size and complexity will grow out of control, confusing even the person who created it.

Overview

Chapter 1. Values, Types, and Operators

Numbers

JavaScript uses 64 bits to store values of type number. Still need to worry about overflow, but only with huge numbers. Floating point arithmetic is still approximate. There are 3 special number types: Infinity, -Infinity and NaN.

Interpolation

Backtick-quoted strings, usually called template literals, can do a few more tricks. Apart from being able to span lines, they can also embed other values.

`half of 100 is ${100 / 2}`;

When you write something inside ${} in a template literal, its result will be computed, converted to a string, and included at that position. The example produces “half of 100 is 50”.

null and undefined

Automatic type conversion

JavaScript goes out of its way to understand almost anything you give it including automatic type conversion when operating on two or more types of different values. When using == to compare different types, JavaScript uses a complicated and confusing set of rules to determine what to do. Use === to avoid automatic type conversion.

Chapter 2. Program Structure

Expressions and Statements

Expressions are fragments of code which produce values e.g. 22, "psycho", (22), typeof 22, 3 + 4 Statements combine expressions. The simplist statements are just expressions followed by a ; e.g. 1;, false;

Bindings

Imagine bindings (or variables) as tentacles, rather than boxes.

A binding name may include dollar signs $ or underscores _ but no other punctuation or special characters. Words with a special meaning, such as let, are keywords, and may not be used as binding names. The full list of keywords and reserved words is long.

 break case catch class const continue debugger default
 delete do else enum export extends false finally for
 function if implements import interface in instanceof let
 new package private protected public return static super
 switch this throw true try typeof var void while with yield

When creating a binding produces an unexpected syntax error, first check if trying to define a reserved word.

The Environment

The collection of bindings and their values which exist at a given time is the environment. Many of these values of of type function. Executing a function = “invoke” it, “call” it or “apply” it.

Blocks

{ and } can be used to group any number of statements into a block.

Capitalization

Most bindings use Lower Camel Case e.g. fuzzyLittleTurtle. Constructor functions use Camel Case e.g. Number.

Chapter 3. Functions

A function is just a regular binding where the value is a function.

const square = function (x) {
  return x * x;
};
console.log(square(4));

Scopes

Declaration notation

Arrow notation

const square1 = (x) => {
  return x * x;
};
const square2 = (x) => x * x;

Parameters

Closures

Closures are possible because:

function wrapValue(n) {
  let local = n; // creates a local binding
  return () => local; // returns a function which accesses this
}
let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1()); // → 1
console.log(wrap2()); // → 2

The local variable from wrapValue isn’t necessary since the parameter itself is a local variable. Therefore equivalent to:

function wrapValue(n) {
  return () => n;
}

Use this idea to create functions which multiply by an arbitrary amount:

function multiplier(factor) {
  return (number) => number * factor; // returns a **function** which accepts one parameter
}
let twice = multiplier(2);
console.log(twice(5)); // → 10

Thinking about programs like this takes some practice. A good mental model is to think of function values as containing both the code in their body and the environment in which they are created. When called, the function body sees the environment in which it was created, not the environment in which it is called.

Functions and side-effects

Functions can be roughly divided into those that are called for their side effects and those that are called for their return value. (Though it is definitely also possible to both have side effects and return a value.)

A pure function is a specific kind of value-producing function that:

Chapter 4. Objects and Arrays

On two occasions I have been asked, ‘Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out?’ […] I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question. — Charles Babbage, Passages from the Life of a Philosopher (1864)

Properties

Objects

let day1 = {
  squirrel: false,
  events: ["work", "touched tree", "pizza", "running"],
};

Arrays

Arrays are simply a kind of object specialized for storing sequences of things, with extra methods available

Strings

Rest parameters

Destructuring

Chapter 5. Higher-Order Functions

function greaterThan(n) {
   return m => m > n; // returns a **function**
}
let greaterThan10 = greaterThan(10);
console.log(greaterThan10(11));
// → true

Chapter 6. The Secret Life of Objects

Encapsulation

Methods

let rabbit = {
  type: "white",
  speak(line) {
    console.log(`The ${this.type} rabbit says ${line}`);
  },
};
rabbit.speak("hi");
speak.call(hungryRabbit, "Burp!");

this

function normalise() {
  console.log(this.coords.map((n) => n / this.length)); // if using `function`, `this` would be undefined
}
normalise.call({ coords: [1, 2, 3] }, (length: 5));
// -> [0, 0.4, 0.6]

Prototypes

Most objects in JavaScript also have a prototype, an informal take on the OO concept of classes. Functions derive from Function.prototype, array from Array.prototype and these prototypes as well as objects derive from Object.prototype

let protoRabbit = {
  speak(line) {
    console.log(`The ${this.type} rabbit says ${line}`);
  },
};
let killerRabbit = Object.create(protoRabbit);
killerRabbit.type = "killer";
killerRabbit.speak("SKRREEE");

Constructor Functions

Special functions - use Object.create - ensure all mandatory properties are initialised.

function makeRabbit(type) {
  let rabbit = Object.create(rabbitPrototype);
  rabbit.type = type;
  return rabbit;
}
let wiseRabbit = makeRabbit("wise");

Equivalently syntax using new.

function Rabbit(type) {
  this.type = type;
}
Rabbit.prototype.speak = function (line) {
  console.log(`The ${this.type} rabbit says ${line}`);
};
let weirdRabbit = new Rabbit("weird");

Constructors (and all functions actually) automatically get a property name prototype, to which you can add further functionality e.g. the function speak.

Pre ES6

JavaScript classes are constructor functions with a prototype property and are written as such.

Post ES6

Same, but with easier syntax.

class Rabbit {
  constructor(type) {
    this.type = type;
  }
  speak(line) = {
    console.log(`The ${this.type} rabbit says ${line}`);
  }
  // can currently only defined methods, not properties (other than directly manipulating the prototype after creation)
}
let blackRabbit = new Rabbit('black');

Maps

Since all objects derive from Object.prototype and inherit several default methods e.g. toString, this can be problematic when requiring a map type object where the ONLY properties you want are those which have been explicitly defined.

The JavaScript Map class solves this. Methods get and set can be used to set keys and retrieve values.

Getter, Setters and Statics

Chapter 8. Bugs and Errors

Chapter 9. Regular Expressions

Some people, when confronted with a problem think ‘I know, I’ll use regular expressions.’ Now they have two problems. — Jamie Zawinski

Useful methods

.test() on RegExp

.test() on RegExp when passed a string, returns a boolean e.g. /abc/.test("abcdef") returns true

Subexpressions are grouped using () and are treated as a single element e.g. /boo+(hoo+)+/i.test("Boohoooohoohoooo" returns true

.exec() on RegExp

.exec() on RegExp returns a “match” object. Either null if not found OR an Array of the matches, with some extra properties including index containing where the match was found (and also the properties input and groups)

.match() on String

.match() on String is the equivalent of .exec() on RegExp

.replace() on String

This String method can take either Strings or RegExps for the first argument. The second argument can additionally use the $1, $2 group substitutions (use $& for the whole match).

console.log("Liskov, Barbara\nMcCarthy, John\nWadler, Philip".replace(/(\w+), (\w+)/g, "$2 $1"));
// → Barbara Liskov
//   John McCarthy
//   Philip Wadler

A function can alternatively be passed as the second arguments

let s = "the cia and fbi";
console.log(s.replace(/\b(fbi|cia)\b/g, (str) => str.toUpperCase()));
// → the CIA and FBI

.search() on String

Similar to indexOf, but using regular expressions instead e.g. " word".search(/\S/) gives 2

The Date class

Example using regex, groups and destructuring to parse variables from a string

function getDate(string) {
  let [_, month, day, year] = /(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string);
  return new Date(year, month - 1, day);
}
console.log(getDate("1-30-2003"));
// → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)

Matching and Backtracking

The reg ex parsing engine looks for a match in the string checking from the first character then the second and so on. When there are choices (using |) or wildcard operators (*, +) and the initial pass doesn’t yield a match, the engine has to backtrack and try a second way (the second branch, or less greedy using of *).

Some regular expressions and string combinations can be written such that ** a lot** backtracking occurs and can yield performance issues. e.g. "0101010101010101010101010101".match(/([01]+)+b/) never matches and takes about 5 seconds to return null. The time to complete is doubled for each number added to the string.

Greed

Due to the mechanics of matching, +, *, ? and {} are greedy by default, i.e. they match as much as they can and backtrack from there.

Using a following ? makes these operators nongreedy i.e. +?, *?. This is often what is wanted in the first place.

When using a repetition operator consider the nongreedy variant first.

The lastIndex property

This lastIndex property is set on a RegExp object where the global or sticky flag is true AND the exec method is used. It keeps track of where the next match will be checked from when exec is called a second or subsequent time.

This can be useful OR lead to bugs.

Looping over matches

let input = "A string with 3 numbers in it... 42 and 88.";
let number = /\b\d+\b/g;
let match;
while ((match = number.exec(input))) {
  console.log("Found", match[0], "at", match.index);
}
// -> Found 3 at 14
// -> Found 42 at 33
// -> Found 88 at 40

Source of bugs

let pattern = /abc/g;
console.log(pattern.exec("abc in the summer"));
// -> [ 'abc', index: 0, input: 'abcdefabc', groups: undefined ]
console.log(pattern.lastIndex);
// -> 3
console.log(pattern.exec("abc in the winter"));
// -> null

International Characters

Regular expressions in JavaScript work with code units NOT actual characters. So 🍎 which is composed of two code units behaves unexpectedly.

console.log(/🍎{3}/.test("🍎🍎🍎"));
// -> false - only the second code unit has the {3} applied, so the test fails

Use /u to treat Unicode characters correctly.

console.log(/🍎{3}/u.test("🍎🍎🍎"));
// -> true

Summary

regex meaning
/abc/ A sequence of characters
/[abc]/ Any character from a set of characters
/[^abc]/ Any character not in a set of characters
/[0-9]/ Any character in a range of characters
/x+/ One or more occurrences of the pattern x
/x+?/ One or more occurrences, nongreedy
/x*/ Zero or more occurrences
/x?/ Zero or one occurrence
/x{2,4}/ Two to four occurrences
/(abc)/ A group
`/a b
/\d/ Any digit character
/\w/ An alphanumeric character (“word character”)
/\s/ Any whitespace character
/./ Any character except newlines
/\b/ A word boundary
/^/ Start of input
/$/ End of input

Chapter 10. Modules

Modules are used to break programs into smaller pieces. Each module should specify its dependencies and its own interface. In a similar way to objects, they expose some functionality publically and keep the rest private.

Relationships between modules are called dependencies.

CommonJS modules

Until 2015, there was no official module system in JavaScript so improvised systems emerged using functions to create local scopes and objects to represent interfaces.

CommonJS is the most widely used of these. The main concept is a function require which is called with a string of the name of the dependency e.g. require("ordinal"). The string is treated as a path. The file system will be searched in obvious places to find a file or folder with the same name e.g. ordinal.js (or index.js by default if a folder is used instead - can be changed in package.json). Strings starting ./, ../ or just / can also be used.

The following steps are taken when requiring a module:

A module object is the result of requiring a file in this way. The module object has a special property exports, itself an object which can be used to expose properties of the module e.g. exports.id = "hello world";. The module.exports object is returned by the require function.

To export functions etc from a module, define on the exports object e.g. exports.formatDate = function(date, format) {...}

ES6 modules