Notes on
Eloquent JavaScript
by Marijn Haverbeke
| 11 min read
Learning is hard work, but everything you learn is yours and will make further learning easier.
There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies.
C.A.R. Hoare, 1980 ACM Turing Award Lecture
Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.
Brian Kernighan and P.J. Plauger, The Elements of Programming Style
Too bad! Same old story! Once you’ve finished building your house you notice you’ve accidentally learned something that you really should have known—before you started.
Friedrich Nietzsche, Beyond Good and Evil
Use the entire language
Some programmers believe that this complexity is best managed by using only a small set of well-understood techniques in their programs. They have composed strict rules (“best practices”) prescribing the form programs should have and carefully stay within their safe little zone.
This is not only boring, it is ineffective. New problems often require new solutions. The field of programming is young and still developing rapidly, and it is varied enough to have room for wildly different approaches. There are many terrible mistakes to make in program design, and you should go ahead and make them at least once so that you understand them. A sense of what a good program looks like is developed with practice, not learned from a list of rules.
Any kind of predetermined “this is how we do things around here” isn’t great. But it isn’t entirely bad, either! Those kinds of heuristics/rules often come from the collective experience of some community, and should be understood before broken.
But I do think that there’s so much room to learn new things & experiment, and that the ‘best practices’ don’t hold forever.
Introduction
JavaScript was introduced in 1995 to add programs to web pages in Netscape Navigator.
After it was adopted outside Netscape, the ECMAScript standard was written to describe the way JavaScript should work. ECMA comes after the Ecma International organization that did the standardization. So ECMAScript and JavaScript refers to the same language.
Values, Types, and Operators
JavaScript uses 64 bits to store a single number value.
With
Given 64 binary digits, you can represent
But not all numbers less than that can fit in a JS number. The bits also store negative numbers – we use one bit to indicate the sign. We also bits to store the position of the decimal point (for nonwhole numbers). So we can actually store about
Calculations with integers below that are guaranteed to be precise.
Calculations with fractional numbers are generally not. We lose precision.
Strings are also modeled as a series of bits.
JS models its approach based on the Unicode standard.
It uses Unicode, but only 16 bits per string (UTF-16 to encode strings. 16 bits can describe up to 216 different characters), which isn’t enough to cover the amount of characters that Unicode defines. So some characters (e.g., many emojis) take two character positions in JS strings.
// Two emoji characters, horse and shoe
let horseShoe = "🐴👟";
console.log(horseShoe.length);
// → 4
console.log(horseShoe[0]);
// → (Invalid half-character)
console.log(horseShoe.charCodeAt(0));
// → 55357 (Code of the half-character)
console.log(horseShoe.codePointAt(0));
// → 128052 (Actual code for horse emoji)
This is very illustrative of the issue with UTF-16 in JS strings. The length says 4
characters, but it’s just two emojis.
And you can’t just index by 0. You’d have to use charPointAt
to get the actual code for the emoji.
To get the actual amount of characters (visible), you’d have to use: const x1 = Array.from("🎉😀🤣😡💰⭐🍵😵")
. Or [..."🍵🍵"]
and then get the length
.
You can iterate through the symbols individually. E.g. just use a for .. of
loop over the string.
Type Coercion
When we apply an operator to the ‘wrong’ type of value, JS will quietly convert the value to the type it needs, using a specified set of rules.
This is called type coercion.
Short-circuiting of logical operators
The logical operators &&
and ||
convert the value on their left side to Boolean type to decide what to do, but depending on the operator and the result of the conversion, they return either the original left-hand value or the right-hand value.
For example, ||
returns the left value when that value can be converted to true
and the right value otherwise.
Similarly, &&
returns the left value when that value can be converted to false
and the right value otherwise.
false && 'yes' // false
true && 'yes' // 'yes'
The ??
operator (nullish coalescing) returns the left value when that value is not null
or undefined
, and the right value otherwise.
Functions
Closures
A function that references bindings from local scopes around it is a closure.
function createCounter(): () => number {
let count = 0;
return function(): number {
count += 1;
return count;
};
}
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // Output: 1
console.log(counter1()); // Output: 2
console.log(counter2()); // Output: 1
console.log(counter2()); // Output: 2
When createCounter
is called, it creates a new execution context with its own count
variable.
The returned function forms a closure over count
, meaning it retains access to it even after createCounter
has finished executing.
Objects
To declare a private method, put a #
sign in front of its name.
Those methods can only be called from inside the class
declaration that defines them.
class SecretiveObject {
#getSecret() {
return "I ate all the plums";
}
interrogate() {
const shallISayIt = this.#getSecret();
return "never";
}
}
Maps
Don’t use objects as maps.
Plain objects derive from Object.prototype
, which means they will have properties that you didn’t define.
You could create objects with no prototypes with Object.create(null)
.
Object property names must be strings. If you need a map whose keys can’t easily be converted to strings, you can’t use objects.
Fortunately, JS has a Map
class. So prefer using it over plain objects for this purpose.
If you do need to use plain objects, know that:
Object.keys
returns only the object’s own keys, not those in the prototype- You can use
Object.hasOwn
, which ignores the object’s prototype (in
does not!).
The Iterator Interface
A for/of
expects an iterable, meaning the object it’s given should have a method with the symbol Symbol.iterator
as its name.
Symbol.iterator
is a symbol defined by the language and is stored as a property of the Symbol
function.
The method should return an object that provides a second interface, iterator. This is what’s being iterated. It has a next
method that returns the next result, which should be an object with a value
property providing the next value, if there is one, and a done
property denoting whether there are no more results by true
and false
otherwise.
const okIterator = "OK"[Symbol.iterator]();
console.log(okIterator.next()); // → {value: "O", done: false}
console.log(okIterator.next()); // → {value: "K", done: false}
console.log(okIterator.next()); // → {value: undefined, done: true}
Symbol
is a primitive datatype & represents a unique identifier. Each Symbol value is guaranteed to be unique and immutable, and as such, are often used as property keys when you want to ensure no naming conflicts.
Symbol.iterator
is one of these symbols. It’s used to define the default iterator for an object. When an object implements the Symbol.iterator
method, it becomes iterable, and can be used in language constructs that expect iterables, like for...of
loops, the ...
spread operator, Array.from()
, and so on.
Regular Expressions
Character Classes
Built-in Shortcuts
\d
: Any digit character (equivalent to[0-9]
)\w
: An alphanumeric character (“word character”)\s
: Any whitespace character (space, tab, newline, etc.)\D
: A character that is not a digit\W
: A nonalphanumeric character\S
: A nonwhitespace character.
: Any character except for newline
Inverted Character Sets
Use ^
after the opening bracket to match any character except those in the set.
let nonBinary = /[^01]/;
console.log(nonBinary.test("1100100010100110")); // → false
console.log(nonBinary.test("0111010112101001")); // → true
Unicode Properties and Support
You can use \p
in regular expressions to match all characters to which the Unicode standard assigns a given property:
\p{L}
: Any letter\p{N}
: Any numeric character\p{P}
: Any punctuation character\P{L}
: Any nonletter (uppercase P inverts)\p{Script=Hangul}
: Any character from the given script
Note: Avoid using \w
for non-English text processing. It doesn’t treat character’s like é
as letters. \p
property groups are more robust.
Make sure you add a u
flag after the regular expression for Unicode support.
Adding u
flag for proper Unicode character handling:
console.log(/🍎{3}/u.test("🍎🍎🍎")); // → true
console.log(/<.>/u.test("<🌹>")); // → true
Quantifiers
{n}
: Exactly n occurrences{n,m}
: Between n and m occurrences{n,}
: n or more occurrences?
: Zero or one occurrence*
: Zero or more occurrences+
: One or more occurrences
Look-ahead Tests
Look-ahead tests check if the input matches a certain pattern without actually moving to the next match position. They are written between (?=
and )
.
- Positive look-ahead:
(?=pattern)
- Negative look-ahead:
(?!pattern)
console.log(/a(?=e)/.exec("braeburn")); // → ["a"]
console.log(/a(?! )/.exec("a b")); // → null
In the first example, the e
is needed to make a match, but it is not included in the matched text.
The (?! )
means a negative look-ahead, which only matches if the pattern in the parentheses does not match. This causes the second example to only match a
characters that do not have a space after them.
Greediness
Motivating this by an example. We may want to remove comments from JS using replace
:
function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*\*\//g, "");
}
However, that output can be incorrect due to the greedy nature of the [^]*
. For example:
console.log(stripComments("1 /* a */+/* b */ 1")); // Incorrectly outputs: 1 1
Greedy operators (+
, *
, ?
, {}
) match as much as possible and backtrack if necessary.
In the example above, [^]*
first matches the entire remaining string, then backtracks to find the nearest */
, leading to incorrect removal of comments.
We can add add ?
after them to make them non-greedy (e.g., *?
). This makes the matcher consume the smallest possible portion that satisfies the pattern:
function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}
console.log(stripComments("1 /* a */+/* b */ 1")); // → 1 + 1
Prefer nongreedy variants (+?
, *?
, ??
, {}?
) when using repetition operators to avoid unintended greedy matches.
By using nongreedy operators, you ensure that the regular expression matches only as much as necessary, avoiding issues with backtracking and incorrect matches.
Dynamically Creating RegExp Objects
When dynamically creating regular expressions, handle special characters to ensure the regex works correctly.
let name = "dea+hl[]rd";
let escaped = name.replace(/[\\[.+*?(){|^$]/g, "\\$&");
let regexp = new RegExp("(^|\\s)" + escaped + "($|\\s)", "gi");
General Patterns
/abc/
: A sequence of characters/[abc]/
: Any character from the set of characters/[^abc]/
: Any character not in the set of characters/[0-9]/
: Any character in a range (e.g., 0 to 9)/x+/
: One or more occurrences of the patternx
/x+?/
: One or more occurrences of the patternx
(nongreedy)/x?/
: Zero or one occurrence of the patternx
/x{2,4}/
: Two to four occurrences of the patternx
/(abc)/
: A group/a|b|c/
: Any one of several patterns/\d/
: Any digit character/\w/
An alphanumeric character (word character)/\s/
: Any whitespace character/./
: Any character except newlines/^/
: Start of input/$/
: End of input
Use appropriate flags (like g
for global, i
for case-insensitive, u
for Unicode) as needed.
Modules
Why bundle: it’s faster than loading a ton of small files because the network overhead will be the bottleneck.
Why we compile JS: to use other dialects, new features that haven’t been released yet.
Why minify: to reduce bundle size.
Asynchronous Programming
The JavaScript environment only runs one program at a time.
Like a big loop around your program (called the event loop).
It’s just waiting for events. And when one arises, it runs it.
The mental model for the event loop is simple: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop
Because no two things run at the same time, slow-running code can delay the handling of other events.
Exceptions in callbacks
Promises have made exception handling much easier.
Asynchronous behavior happens on its own empty function call stack.
Since each callback starts with a mostly empty stack, catch
handlers won’t be on the stack when they throw an exception.
So this error won’t get caught:
try {
setTimeout(() => {
throw new Error("Erroring");
}, 20);
} catch (e) {
// Won't run:
console.log("Catching error", e);
}
Events
- Event propagation: Handlers on parent nodes with children receive events from children
- If both they both the child and parent has an event handler, then the specific handlers take precedence over general ones
- Event propagates outward from where it occurred to parent nodes and document root, calling the most specific handlers first
stopPropagation
method can prevent event from reaching higher handlers
- Many events have default actions (e.g., clicking a link takes you to the target, pressing down arrow scrolls down, right-click shows context menu).
preventDefault
can be used to not run default event actions
Liked these notes? Join the newsletter.
Get notified whenever I post new notes.