Exploring JavaScript (2024)
Exploring JavaScript (2024)
I BACKGROUND
1.4 Acknowledgements
3 Why JavaScript?
7 FAQ: JavaScript
7.2 How do I find out what JavaScript features are supported where?
7.3 Where can I look up what features are planned for JavaScript?
outdated features?
II FIRST STEPS
9 Syntax
9.2 (Advanced)
9.4 Identifiers
9.7 Semicolons
11 Assertion API
12.1 Exercises
13.1 let
13.2 const
13.5 (Advanced)
13.9 Closures
14 Values
14.5 The operators typeof and instanceof: what’s the type of a value?
15 Operators
IV PRIMITIVE VALUES
16 The non-values undefined and null
17 Booleans
18 Numbers
18.1 Numbers are used for both floating point numbers and integers
18.7 (Advanced)
19 Math
19.1 Data properties
19.3 Rounding
19.6 Sources
[ES2020]
20 Bigints – arbitrary-precision integers (advanced)
20.2 Bigints
22 Strings
clusters
[ES6]
23 Using template literals and tagged templates
23.6 (Advanced)
[ES6]
24 Symbols
[ES1]
25.3 if statements
[ES3]
25.4 switch statements
[ES1]
25.5 while loops
[ES3]
25.6 do-while loops
[ES1]
25.7 for loops
[ES6]
25.8 for-of loops
[ES2018]
25.9 for-await-of loops
[ES1]
25.10 for-in loops (avoid)
26 Exception handling
26.2 throw
27 Callable values
[ES6]
27.3 Specialized functions
28.1 eval()
28.3 Recommendations
VI MODULARITY
[ES6]
29 Modules
29.8 Re-exporting
[ES2020]
29.13 import.meta – metadata for the current module
[ES2020]
29.14 Loading modules dynamically via import() (advanced)
[ES2022]
29.15 Top-level await in modules (advanced)
30 Objects
[ES2018]
30.4 Spreading into object literals (...)
(advanced)
[ES5]
30.8 Property attributes and property descriptors (advanced)
[ES5]
30.9 Protecting objects from being changed (advanced)
[ES6]
31 Classes
[ES2022]
31.5 Instance members of classes
31.7 Subclassing
VII COLLECTIONS
[ES6]
32 Synchronous iteration
[ES2024]
32.5 Grouping iterables
33 Arrays (Array)
.flatMap()
[ES6]
34 Typed Arrays: handling binary data (advanced)
[ES2024]
34.6 Resizing ArrayBuffers
[ES2024]
34.7 Transferring and detaching ArrayBuffers
[ES6]
35 Maps (Map)
35.1 Using Maps
[ES6]
36 WeakMaps (WeakMap) (advanced)
[ES6]
37 Sets (Set)
[ES6]
38 WeakSets (WeakSet) (advanced)
[ES6]
39 Destructuring
39.4 Object-destructuring
39.5 Array-destructuring
39.9 (Advanced)
[ES6]
40 Synchronous generators (advanced)
VIII ASYNCHRONICITY
41.7 Resources
[ES6]
42 Promises for asynchronous programming
42.2 Examples
42.3 Error handling: don’t mix rejections and exceptions
asynchronously
Promises
[ES2017]
43 Async functions
43.4 (Advanced)
[ES2018]
44 Asynchronous iteration
45.14 The flags /g and /y, and the property .lastIndex (advanced)
46 Dates (Date)
47.5 FAQ
X MISCELLANEOUS TOPICS
XI APPENDICES
A Index
I Background
1 Before you buy the book
1.2.1 How can I preview the book and its bundled material?
1.4 Acknowledgements
Highlights:
program.
1.2.1 How can I preview the book and its bundled material?
Most material has free preview versions (with about 50% of their
The homepage of Exploring JavaScript explains how you can buy one of its
digital packages.
has been developing web applications since 1995. In 1999, he was technical
Since 2011, he has been blogging about web development at 2ality.com and
has written several books on JavaScript. He has held trainings and talks for
1.4 Acknowledgements
Allen Wirfs-Brock
Benedikt Meurer
Brian Terlson
Daniel Ehrenberg
Jordan Harband
Maggie Johnson-Pint
Mathias Bynens
Myles Borins
Rob Palmer
Šime Vidas
Johannes Weber
2 FAQ: book and supplementary
material
“(advanced)”?
Payhip?
programmers”)
This chapter answers questions you may have and gives tips for reading this
book.
2.1 How to read this book
quick references.
“mode”, you read everything and don’t skip advanced content and
quick references.
It serves as a reference. If there is a topic that you are interested in, you
can find information on it via the table of contents or via the index.
Due to basic and advanced content being mixed, everything you need
Exercises play an important part in helping you practice and retain what you
have learned.
2.1.2 Why are some chapters and sections marked with “(advanced)”?
Several chapters and sections are marked with “(advanced)”. The idea is
that you can initially skip them. That is, you can get a quick working
As your knowledge evolves, you can later come back to some or all of the
advanced content.
2.2 I own a digital version
The HTML version of this book (online, or ad-free archive in the paid
version) has a link at the end of each chapter that enables you to give
feedback.
The receipt email for the purchase includes a link. You’ll always be
If you opted into emails while buying, you’ll get an email whenever
there is new content. To opt in later, you must contact Payhip (see
bottom of payhip.com).
programmers”)
If you bought the print version, you can get a discount for a digital version.
JavaScript” and check the latest release of this book. The error may
If the error is still there, you can use the comment link at the end of
especially the static types number of num and boolean of the result, are not real
Why is this notation being used? It helps give you a quick idea of how a
Reading instructions
External content
Tip
Question
FAQ).
Warning
similar to a footnote.
Exercise
point.
3 Why JavaScript?
3.2.1 Community
3.2.3 Language
Among programmers, JavaScript isn’t always well liked. One reason is that
it has a fair amount of quirks. Some of them are just unusual ways of doing
something. Others are considered bugs. Either way, learning why JavaScript
does something the way it does, helps with dealing with the quirks and with
accepting JavaScript (maybe even liking it). Hopefully, this book can be of
assistance here.
Additionally, many traditional quirks have been eliminated now. For
example:
introduced let and const, which let you declare block-scoped variables.
and .prototype was clumsy. ES6 introduced classes, which provide more
Many libraries are easily available via the npm software registry.
3.2.1 Community
JavaScript’s popularity means that it’s well supported and well documented.
Whenever you create something in JavaScript, you can rely on many people
With JavaScript, you can write apps for many client platforms. These are a
React Native lets you write apps for iOS and Android that have native
user interfaces.
Node.js provides extensive support for writing shell scripts (in addition
example:
ZEIT Now
AWS Lambda
There are many data technologies available for JavaScript: many databases
Lastly, many, if not most, tools for JavaScript are written in JavaScript. That
includes IDEs, build tools, and more. As a consequence, you install them
the same way you install your libraries and you can customize them in
JavaScript.
3.2.3 Language
Many libraries are available, via the de-facto standard in the JavaScript
more features:
You can work with ReasonML, which is, roughly, OCaml with
software, and more. The upside is that you will constantly learn new things.
Thankfully, things have somewhat slowed down, recently: all of ES6 (which
was a considerable modernization of the language) is becoming established,
programming languages:
function).
Perl.
quasi literals).
types and you can assign any value to a given (mutable) variable.
classes, etc.
(rewritten to require less storage). And there are plans for a binary
JavaScript is part of the web platform – it is the language built into web
not an exception.
> 1 / 0
Infinity
The reason for the silent failures is historical: JavaScript did not have
exceptions until ECMAScript 3. Since then, its designers have tried to avoid
silent failures.
These are a few tips to help you get started with JavaScript:
Take your time to really get to know this language. The conventional
You can check for problems and anti-patterns via linters such as
ESLint.
You can format your code automatically via code formatters such
as Prettier.
between the spoken and the written word, it is well suited for
exchanging knowledge.
ECMAScript versions
ECMAScript version?
Netscape Navigator.
The idea was that major interactive parts of the client-side web were to be
those parts and to also make HTML slightly more interactive. Given its role
of assisting Java, JavaScript had to look like Java. That ruled out existing
LiveScript.
name, JavaScript.
JavaScript. A different name was chosen because Sun (now Oracle) had a
trademark for the latter name. The “ECMA” in “ECMAScript” comes from
versions.
ECMAScript 5.1 (June 2011): Another small update to keep Ecma and
release life cycle resulted in fewer new features compared to the large
ES6.
June.
Committee 39)
TC39 is the committee that evolves JavaScript. Its members are, strictly
Opera, Twitter, and others. That is, companies that are usually competitors
and invited experts attend. The minutes of those meetings are public in a
GitHub repository.
With ECMAScript 6, two issues with the release process used at that time
became obvious:
If too much time passes between releases then features that are ready
early, have to wait a long time until they can be released. And features
that are ready late, risk being rushed to make the deadline.
Features were often designed long before they were implemented and
stages: a strawperson stage 0 and five “maturity” stages (1, 2, 2.7, 3, 4).
implementations.
ECMAScript versions are released once per year and include all
The result: smaller, incremental releases, whose features have already been
field-tested.
ES2016 was the first ECMAScript version that was designed according to
versions
ES6 yet?”
feature reaches stage 4, we can safely use it (if it’s supported by the
JavaScript engines we are targeting). We don’t have to wait until the next
ECMAScript release.
ECMAScript features are designed via proposals that go through the so-
Stage 0 means a proposal has yet to enter the actual process. This is
Then the proposal goes through the five maturity stages 1, 2, 2.7, 3 and
ECMAScript standard.
to the specification.
Each stage has entrance criteria regarding the state of the artifacts:
1 draft
2 finished draft
2.7 finished
3 finished prototypes
4 2 implementations
Champion: Each proposal has one or more TC39 delegates that guide
and must sign off on it before the proposal can reach stage 2.7. They
proposal).
specification.
Not part of the usual advancement process. Any author can create
Entrance criteria:
Pick champions
Status:
Entrance criteria:
Proposal is complete.
Draft of specification.
Status:
Entrance criteria:
editors.
Status:
validation.
Entrance criteria:
Status:
standard
Entrance criteria:
implementations
Status:
ECMAScript specification.
finished specification
two implementations
production.
old features and quirks. While the appeal of that idea is obvious, it has
significant downsides.
compatible and fixes all of its flaws. As a result, we’d encounter the
following problems:
JavaScript engines become bloated: they need to support both the old
and the new version. The same is true for tools such as IDEs and build
tools.
We can either migrate all of an existing code base to the new version
changing it.
in ES6).
ECMAScript version?
There are several places where you can look up what’s new in each
ECMAScript version:
In this book, there is a chapter that lists what’s new in each
The TC39 repository has a table with finished proposals that states in
If you are wondering what stages various proposed features are in, consult
Stage 2.7 was added in late 2023, after stages 0, 1, 2, 3, 4 had already been
Q: How about 3a for the new stage and 3b for the old stage 3?
as to whether this refers to the new stage 3a or the new stage 3b.
Source: TC39 discussion on 2023-11-30
6 New JavaScript features
chronological order. It ends before ES6 (ES2015): ES2016 was the first
truly incremental release of ECMAScript – which is why ES6 has too many
features to list here. If you want to get a feeling for earlier releases:
(ES2015).
assert.deepEqual(
Map.groupBy([0, -5, 3, -4, 8, 9], x =>
Math.sign(x)),
new Map()
.set(0, [0])
.set(-1, [-5,-4])
.set(1, [3,8,9])
);
Map:
assert.deepEqual(
Object.groupBy([0, -5, 3, -4, 8, 9], x =>
Math.sign(x)),
{
'0': [0],
'-1': [-5,-4],
'1': [3,8,9],
__proto__: null,
}
);
want to resolve:
const { promise, resolve, reject } =
Promise.withResolvers();
points):
😵💫
assert.equal(
/^\p{Emoji}$/u.test(' '), false
);
// New: Unicode string property `RGI_Emoji`
via /v
😵💫
assert.equal(
/^\p{RGI_Emoji}$/v.test(' '), true
);
😵💫
> /^[\q{ }]$/v.test(' ') 😵💫
true
> /^[\q{abc|def}]$/v.test('abc')
true
> /^[\w--[a-g]]$/v.test('a')
false
> /^[\p{Number}--[0-9]]$/v.test('٣')
😵💫 😵💫')
true
> /^[\p{RGI_Emoji}--\q{ }]$/v.test('
false
SharedArrayBuffers can be resized, but they can only grow and never
shrink. They are not transferrable and therefore don’t get the method
each lone surrogate is replaced with the code unit 0xFFFD (which
represents the code point with the same number, whose name is
memory. Its functionality is beyond the scope of this book. See the
“Change Array by copy”: Arrays and Typed Arrays get new non-
square brackets:
“Array find from last”: Arrays and Typed Arrays get two new methods:
Array:
> ['', 'a', 'b', ''].findLast(s => s.length >
0)
'b'
end of an Array:
starts with a hash (#) and a bang (!). Some JavaScript runtimes, such as
Node.js, have done this for a long time. Now it is also part of the
#!/usr/bin/env node
Private slot checks (“ergonomic brand checks for private fields”): The
#privateSlot in obj
Top-level await in modules: We can now use await at the top levels of
error.cause: Error and its subclasses now let us specify which error
index (like the bracket operator []) and supports negative indices
string
Array
All Typed Array classes: Uint8Array etc.
produces match objects that record the start and end index of each
group capture.
string):
fulfilled. If there are only rejections, they are put into an AggregateError
a ||= b
a &&= b
a ??= b
Underscores (_) as separators in:
WeakRefs: This feature is beyond the scope of this book. Quoting its
proposal states:
functionality:
Their correct use takes careful thought, and they are best avoided
if possible.
static: We can only use it at the top levels of modules and its
object.
value?.prop
value ?? defaultValue
something is missing.
Previously the Logical Or operator (||) was used in this case but it has
downsides here because it returns the default value whenever the left-
Promise that is fulfilled once all the input Promises are settled. The
fulfillment value is an Array with one object per input Promise – either
one of:
globalThis provides a way to access the global object that works both on
for-in mechanics: This feature is beyond the scope of this book. For
Namespace re-exporting:
Array method .flatMap() works like .map() but lets the callback return
Arrays are then concatenated and become the result of .flatMap(). Use
cases include:
Filtering and mapping at the same time
flattening.
String methods: .trimStart() and .trimEnd() work like .trim() but remove
elements are considered equal by sorting, then sorting does not change
access an item.
literal, we can copy the properties of another object into the current
can now use rest syntax (...) to get all previously unmentioned
properties in an object.
related to the try clause and .catch() is related to the catch clause.
preceded by 'X'.
s (dotAll) flag for regular expressions. If this flag is active, the dot
> /^\p{Lowercase_Letter}+$/u.test('aüπ')
true
> /^\p{White_Space}+$/u.test('\n \t')
true
> /^\p{Script=Greek}+$/u.test('ΩΔΨ')
true
have been allowed in Arrays literals since ES3 and in Object literals
since ES5. They are now also allowed in function calls and method
calls.
Docs
> 4 ** 2
16
ECMAScript feature lists were taken from the TC39 page on finished
proposals.
7 FAQ: JavaScript
where?
JavaScript?
outdated features?
where?
Mozilla’s MDN web docs have tables for each feature that describe
JavaScript?
not an exception.
> 1 / 0
Infinity
The reason for the silent failures is historical: JavaScript did not have
exceptions until ECMAScript 3. Since then, its designers have tried to avoid
silent failures.
This question is answered in “How to not break the web while changing
JavaScript” (§5.6).
In this chapter, I’d like to paint the big picture: what are you learning in this
book, and how does it fit into the overall landscape of web development?
This book teaches the JavaScript language. It focuses on just the language,
used:
Web browser
Node.js
You can also use it to write software for the command line (think Unix
installing tools (such as compilers and build tools) and libraries – even
for client-side development.
JS standard
Platform API
library
Figure 8.1: The structure of the two JavaScript platforms web browser
and Node.js. The APIs “standard library” and “platform API” are
platform-specific “core”.
The structures of the two JavaScript platforms web browser and Node.js are
online resources:
MDN Web Docs: cover various web technologies such as CSS, HTML,
development:
JavaScript”.
9.1.2 Modules
9.1.3 Classes
9.2 (Advanced)
9.4 Identifiers
9.5.1 Statements
9.5.2 Expressions
9.6.3 Disambiguation
9.7 Semicolons
9.7.1 Rule of thumb for semicolons
This is a very first look at JavaScript’s syntax. Don’t worry if some things
don’t make sense, yet. They will all be explained in more detail later in this
book.
9.1.1.1 Comments
// single-line comment
/*
Comment with
multiple lines
*/
9.1.1.2 Primitive (atomic) values
Booleans:
true
false
Numbers:
1.141
-123
The basic number type is used for both floating point numbers (doubles)
and integers.
Bigints:
17n
-49n
The basic number type can only properly represent integers within a range
Strings:
'abc'
"abc"
`String with interpolated values: ${256} and
${true}`
JavaScript has no extra type for characters. It uses strings to represent them.
9.1.1.3 Assertions
example, the following assertion states that the result of the computation 7
plus 1 must be 8:
assert.equal(7 + 1, 8);
with two arguments: the actual result and the expected result. It is part of a
9.1.1.5 Operators
// Comparison operators
assert.equal(3 < 4, true);
assert.equal(3 <= 4, true);
assert.equal('abc' === 'abc', true);
assert.equal('abc' !== 'def', true);
immediately and we can’t assign a different value later. However, the value
itself may be mutable and we may be able to change its contents. In other
// Equivalent to add2:
const add3 = (a, b) => a + b;
The previous code contains the following two arrow functions (the terms
9.1.1.10 Arrays
Conditional statement:
if (x < 0) {
x = -x;
}
for-of loop:
Output:
a
b
9.1.2 Modules
Each module is a single file. Consider, for example, the following two files
file-tools.mjs
main.mjs
The module in file-tools.mjs exports its function isTextFilePath():
The module in main.mjs imports the whole module path and the function
isTextFilePath():
9.1.3 Classes
class Person {
constructor(name) {
this.name = name;
}
describe() {
return `Person named ${this.name}`;
}
static logNames(persons) {
for (const person of persons) {
console.log(person.name);
}
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
describe() {
return super.describe() +
` (${this.title})`;
}
}
function throwsException() {
throw new Error('Problem!');
}
function catchesException() {
try {
throwsException();
} catch (err) {
assert.ok(err instanceof Error);
assert.equal(err.message, 'Problem!');
}
}
Note:
We can throw any value, but features such as stack traces are only
identifier.
$, _
Some words have special meaning in JavaScript and are called reserved.
const if = 123;
// SyntaxError: Unexpected token if
But they are allowed as names of properties:
Lowercase:
Methods: obj.myMethod
CSS:
Uppercase:
Classes: MyClass
All-caps:
case)
arr.map((_x, i) => i)
class ValueWrapper {
constructor(value) {
this._value = value;
}
}
const x = 123;
func();
while (false) {
// ···
} // no semicolon
function func() {
// ···
} // no semicolon
9.2 (Advanced)
which executable should be used to run the script. These two characters
comments by most shell scripting languages and JavaScript does so, too.
#!/usr/bin/env node
If we want to pass arguments to node, we have to use the env option -S (to be
9.4 Identifiers
First character:
Subsequent characters:
Examples:
const ε = 0.0001;
const строка = '';
let _tmp = 0;
const $foo2 = true;
Reserved words can’t be variable names, but they can be property names.
await break case catch class const continue debugger default delete do else
export extends finally for function if import in instanceof let new return
static super switch this throw try typeof var void while with yield
The following tokens are also keywords, but currently not used in the
language:
Technically, these words are not reserved, but you should avoid them, too,
that can cause problems because the same syntax can mean different things,
For the sake of simplicity, we pretend that there are only statements
9.5.1 Statements
A statement is a piece of code that can be executed and performs some kind
let myStr;
if (myBool) {
myStr = 'Yes';
} else {
myStr = 'No';
}
9.5.2 Expressions
The operator _?_:_ used between the parentheses is called the ternary
The current location within JavaScript source code determines which kind
expression statements. The opposite is not true: when the context requires
The following code demonstrates that any expression bar() can be either
function f() {
console.log(bar()); // bar() is expression
bar(); // bar(); is (expression) statement
}
function id(x) {
return x;
}
empty object.
{
}
9.6.3 Disambiguation
function expression?
Output:
abc
In this code:
Later in this book, we’ll see more examples of pitfalls caused by syntactic
ambiguity:
Assigning via object destructuring
9.7 Semicolons
const x = 3;
someFunction('abc');
i++;
function foo() {
// ···
}
if (y > 0) {
// ···
}
The whole const declaration (a statement) ends with a semicolon, but inside
it, there is an arrow function expression. That is, it’s not the statement per se
that ends with a curly brace; it’s the embedded arrow function expression.
The body of a control statement is itself a statement. For example, this is the
while (condition)
statement
But blocks are also statements and therefore legal bodies of control
statements:
while (a > 0) {
a--;
}
If you want a loop to have an empty body, your first option is an empty
A semicolon
In other words, ASI can be seen as inserting semicolons at line breaks. The
The good news about ASI is that – if you don’t rely on it and always write
semicolons – there is only one pitfall that you need to be aware of. It is that
JavaScript forbids line breaks after some tokens. If you do insert a line
The token where this is most practically relevant is return. Consider, for
return
{
first: 'jane'
};
That is:
Empty statement: ;
In some cases, ASI is not triggered when you think it should be. That makes
life more complicated for people who don’t like semicolons because they
need to be aware of those cases. The following are three examples. There
are more.
a = b + c
(d + e).print()
Parsed as:
a = b + c(d + e).print();
a = b
/hi/g.exec(c).map(d)
Parsed as:
a = b / hi / g.exec(c).map(d);
someFunction()
['ul', 'ol'].map(x => x + x)
Executed as:
I like the visual structure it gives code – you clearly see where a
statement ends.
recommend that you use tools to help you avoid mistakes. The following are
two examples:
encounters a line that starts with a square bracket, it prefixes that line
with a semicolon.
The static checker ESLint has a rule that you tell your preferred style
can be executed:
Normal “sloppy” mode is the default in scripts (code fragments that are
Strict mode is the default in modules and classes, and can be switched
almost always located in modules. In this book, I assume that strict mode is
'use strict';
The neat thing about this “directive” is that ECMAScript versions before 5
You can also switch on strict mode for just a single function:
function functionInStrictMode() {
'use strict';
}
Let’s look at three things that strict mode does better than sloppy mode. Just
in this one section, all code fragments are executed in sloppy mode.
9.10.2.1 Sloppy mode pitfall: changing an undeclared variable creates a global variable
variable.
function sloppyFunc() {
undeclaredVar1 = 123;
}
sloppyFunc();
// Created global variable `undeclaredVar1`:
assert.equal(undeclaredVar1, 123);
Strict mode does it better and throws a ReferenceError. That makes it easier to
detect typos.
function strictFunc() {
'use strict';
undeclaredVar2 = 123;
}
assert.throws(
() => strictFunc(),
{
name: 'ReferenceError',
message: 'undeclaredVar2 is not defined',
});
mode
function strictFunc() {
'use strict';
{
function foo() { return 123 }
}
return foo(); // ReferenceError
}
assert.throws(
() => strictFunc(),
{
name: 'ReferenceError',
message: 'foo is not defined',
});
function sloppyFunc() {
{
function foo() { return 123 }
}
return foo(); // works
}
assert.equal(sloppyFunc(), 123);
9.10.2.3 Sloppy mode doesn’t throw exceptions when changing immutable data
In strict mode, you get an exception if you try to change immutable data:
function strictFunc() {
'use strict';
true.prop = 1; // TypeError
}
assert.throws(
() => strictFunc(),
{
name: 'TypeError',
message: "Cannot create property 'prop' on
boolean 'true'",
});
function sloppyFunc() {
true.prop = 1; // fails silently
return true.prop;
}
assert.equal(sloppyFunc(), undefined);
For more information on how sloppy mode differs from strict mode,
see MDN.
10 Consoles: interactive JavaScript
command lines
You have many options for quickly running pieces of JavaScript code. The
you can print text via console.log() and where you can run pieces of code.
How to open the console differs from browser to browser. Figure 10.1 shows
To find out how to open the console in your web browser, you can do a web
Apple Safari
Google Chrome
Microsoft Edge
Mozilla Firefox
open (in the bottom half of window) while visiting a web page.
REPL stands for read-eval-print loop and basically means command line. To
use it, you must first start Node.js from an operating system command line,
via the command node. Then an interaction with it looks as depicted in figure
10.2: The text after > is input from the user; everything else is output from
Node.js.
command line).
> 3 + 5
8
There are many web apps that let you experiment with JavaScript in
hidden. For Node.js, the console is the terminal that Node.js is currently
running in.
The full console.* API is documented on MDN web docs and on the Node.js
In this chapter, we only look at the following two methods for printing data
console.log()
console.error()
The first variant prints (text representations of) values on the console:
Output:
Output:
These are some of the directives you can use for substitutions:
abc 123
Output:
Output:
{"foo":123,"bar":"abc"}
%% inserts a single %.
console.log('%s%%', 99);
Output:
99%
error information. For Node.js, that means that the output goes to stderr
Output:
{
"first": "Jane",
"last": "Doe"
}
11 Assertion API
This assertion states that the expected result of 3 plus 5 is 8. The import
function id(x) {
return x;
}
assert.equal(id('abc'), 'abc');
really work.
The exercises for this book are test-driven, via the test framework Mocha.
The strict equal() uses === to compare values. Therefore, an object is only
equal to itself – even if another object has the same content (because ===
assert.equal(3+3, 6);
assert.notEqual(3+3, 22);
The optional last parameter message can be used to explain what is asserted.
If the assertion fails, the message is used to set up the AssertionError that is
thrown.
let e;
try {
const x = 3;
assert.equal(x, 8, 'x must be equal to 8')
} catch (err) {
assert.equal(
String(err),
'AssertionError [ERR_ASSERTION]: x must be equal
to 8');
}
11.4.2 Deep equality: assert.deepEqual()
thrown.
assert.deepEqual([1,2,3], [1,2,3]);
assert.deepEqual([], []);
thrown.
assert.notDeepEqual([1,2,3], [1,2]);
This function calls its first parameter, the function callback, and only
assert.throws(
() => {
null.prop;
},
TypeError
);
assert.throws(
() => {
null.prop;
},
/^TypeError: Cannot read properties of null \
(reading 'prop'\)$/
);
assert.throws(
() => {
null.prop;
},
{
name: 'TypeError',
message: "Cannot read properties of null
(reading 'prop')",
}
);
assert.fail(messageOrError?)
different value.
try {
functionThatShouldThrow();
assert.fail();
} catch (_) {
// Success
}
12 Getting started with exercises
12.1 Exercises
Throughout most chapters, there are boxes that point to exercises. These are
12.1 Exercises
All exercises in this book are tests that are run via the test framework
id_test.mjs (tests)
The key thing here is: everything we want to test must be exported.
You don’t need to worry about the exact details of tests: They are
always implemented for you. Therefore, you only need to read them,
// npm t demos/exercises/id_test.mjs
suite('id_test.mjs');
The core of this test file is line D – an assertion: assert.equal() specifies that
The comment at the very beginning shows the shell command for
mode).
(assertions, etc.).
npm t demos/exercises/id_test.mjs
The t is an abbreviation for test. That is, the long version of this command
is:
The following exercise gives you a first taste of what exercises are
like:
exercises/exercises/first_module_test.mjs
Reading
You may want to postpone reading this section until you get to the
Writing tests for asynchronous code requires extra work: The test receives
its results later and has to signal to Mocha that it isn’t finished yet when it
If the callback we pass to test() has a parameter (e.g., done), Mocha switches
timeout.
function dividePromise(x, y) {
return new Promise((resolve, reject) => {
if (y === 0) {
reject(new Error('Division by zero'));
} else {
resolve(x / y);
}
});
}
is used to fulfill the Promise returned by this async function. And if the test
code throws an exception, then the async function takes care of rejecting the
returned Promise.
III Variables and values
13 Variables and assignment
13.1 let
13.2 const
13.5 (Advanced)
[ES2020]
13.7.1 globalThis
13.9 Closures
Before ES6, there was also var. But it has several quirks, so it’s best to avoid
13.1 let
let i;
i = 0;
i = i + 1;
assert.equal(i, 1);
let i = 0;
13.2 const
Variables declared via const are immutable. You must always initialize
immediately:
assert.throws(
() => { i = i + 1 },
{
name: 'TypeError',
message: 'Assignment to constant variable.',
}
);
In JavaScript, const only means that the binding (the association between
variable name and variable value) is immutable. The value itself may be
each iteration:
Output:
hello
world
let indicates that the value of a variable changes. Use it only when you
exercises/variables-assignment/const_exrc.mjs
{ // // Scope A. Accessible: x
const x = 0;
assert.equal(x, 0);
{ // Scope B. Accessible: x, y
const y = 1;
assert.equal(x, 0);
assert.equal(y, 1);
{ // Scope C. Accessible: x, y, z
const z = 2;
assert.equal(x, 0);
assert.equal(y, 1);
assert.equal(z, 2);
}
}
}
// Outside. Not accessible: x, y, z
assert.throws(
() => console.log(x),
{
name: 'ReferenceError',
message: 'x is not defined',
}
);
Each variable is accessible in its direct scope and all scopes nested within
that scope.
The variables declared via const and let are called block-scoped because
You can’t declare the same variable twice at the same level:
assert.throws(
() => {
eval('let x = 1; let x = 2;');
},
{
name: 'SyntaxError',
message: "Identifier 'x' has already been
declared",
});
Why eval()?
eval() delays parsing (and therefore the SyntaxError), until the callback
executed.
You can, however, nest a block and use the same variable name x that you
const x = 1;
assert.equal(x, 1);
{
const x = 2;
assert.equal(x, 2);
}
assert.equal(x, 1);
Inside the block, the inner x is the only accessible variable with that name.
The inner x is said to shadow the outer x. Once you leave the block, you can
13.5 (Advanced)
function f() {
const x = 3;
// ···
}
x is statically (or lexically) scoped. That is, its scope is fixed and doesn’t
change at runtime.
function g(x) {}
function h(y) {
if (Math.random()) g(y); // (A)
}
Whether or not the function call in line A happens, can only be decided at
runtime.
The scopes directly contained in that scope are the children of the root.
And so on.
The root is also called the global scope. In web browsers, the only location
where one is directly in that scope is at the top level of a script. The
variables of the global scope are called global variables and accessible
They can only be created while at the top level of a script, via
object.
They are created in the top level of a script, via var and function
declarations.
The global object can be accessed via the global variable
variables.
variables.
The following HTML fragment demonstrates globalThis and the two kinds of
global variables.
<script>
const declarativeVariable = 'd';
var objectVariable = 'o';
</script>
<script>
// All scripts share the same top-level scope:
console.log(declarativeVariable); // 'd'
console.log(objectVariable); // 'o'
Each ECMAScript module has its own scope. Therefore, variables that exist
at the top level of a module are not global. Figure 13.1 illustrates how the
[ES2020]
13.7.1 globalThis
The global variable globalThis is the new standard way of accessing the
global object. It got its name from the fact that it has the same value as this
platforms:
window: The classic way of referring to the global object. But it doesn’t
supported by Node.js.
globalThis ✔ ✔ ✔
window ✔
self ✔ ✔
global ✔
The global object is now considered a mistake that JavaScript can’t get rid
generally confusing.
const, let, and class declarations don’t create global object properties
properties of globalThis. The former has always worked the same on all
JavaScript platforms.
omit it:
window.encodeURIComponent(str); // no
encodeURIComponent(str); // yes
Therefore, there are relatively few use cases for globalThis – for example:
supports.
{
console.log(x); // What happens here?
const x;
}
3. There is an error.
let uses the same approach 3 as const, so that both work similarly and it’s
The time between entering the scope of a variable and executing its
The next example shows that the temporal dead zone is truly temporal
(related to time):
Even though func() is located before the declaration of myVar and uses that
variable, we can call func(). But we have to wait until the temporal dead
regardless of where it is located within that scope. That enables you to call a
assert.equal(foo(), 123); // OK
function foo() { return 123; }
The early activation of foo() means that the previous code is equivalent to:
If you declare a function via const or let, then it is not activated early. In the
following example, you can only use bar() after its declaration.
assert.throws(
() => bar(), // before declaration
ReferenceError);
function f() (in the same scope) if we adhere to the following rule: f() must
The functions of a module are usually invoked after its complete body is
executed. Therefore, in modules, you rarely need to worry about the order of
functions.
rule: when entering a scope, all function declarations are executed first,
you need to be careful that it doesn’t access data that isn’t activated early.
funcDecl();
The problem goes away if you make the call to funcDecl() after the
declaration of MY_STR.
We have seen that early activation has a pitfall and that you can get most of
its benefits without using it. Therefore, it is better to avoid early activation.
But I don’t feel strongly about this and, as mentioned before, often use
Even though they are similar to function declarations in some ways, class
assert.throws(
() => new MyClass(),
ReferenceError);
class MyClass {}
this:
var is an older way of declaring variables that predates const and let (which
var x = 123;
function f() {
// Partial early activation:
assert.equal(x, undefined);
if (true) {
var x = 123;
// The assignment is executed in place:
assert.equal(x, 123);
}
// Scope is function, not block:
assert.equal(x, 123);
}
13.9 Closures
Before we can explore closures, we need to learn about bound variables and
free variables.
Per scope, there is a set of variables that are mentioned. Among these
variables we distinguish:
Bound variables are declared within the scope. They are parameters
Free variables are declared externally. They are also called non-local
variables.
function func(x) {
const y = 123;
console.log(z);
}
What is the point of keeping this connection? It provides the values for the
function funcFactory(value) {
return () => {
return value;
};
}
const func = funcFactory('abc');
assert.equal(func(), 'abc'); // (A)
funcFactory returns a closure that is assigned to func. Because func has the
connection to the variables at its birth place, it can still access the free
scope).
function is a closure.
The following function returns incrementors (a name that I just made up).
called, it updates that number by adding the argument to it and returns the
new value.
function createInc(startValue) {
return (step) => { // (A)
startValue += step;
return startValue;
};
}
const inc = createInc(5);
assert.equal(inc(2), 7);
We can see that the function created in line A keeps its internal number in
the free variable startValue. This time, we don’t just read from the birth
scope, we use it to store data that we change and that persists across
function calls.
We can create more storage slots in the birth scope, via local variables:
function createInc(startValue) {
let index = -1;
return (step) => {
startValue += step;
index++;
return [index, startValue];
};
}
const inc = createInc(5);
assert.deepEqual(inc(2), [0, 7]);
assert.deepEqual(inc(2), [1, 9]);
assert.deepEqual(inc(2), [2, 11]);
And they can provide private data for objects (produced via literals or
classes). The details of how that works are explained in Exploring ES6.
14 Values
14.4.2 Objects
value?
14.5.1 typeof
14.5.2 instanceof
For this chapter, I consider types to be sets of values – for example, the type
(any)
null number
Array Function
string
Map RegExp
symbol
Set Date
classes for errors, the classes associated with primitive types, and
more. The diagram hints at the fact that not all objects are instances of
Object.
Figure 14.1 shows JavaScript’s type hierarchy. What do we learn from that
diagram?
Object – for example, objects created via object literals. More details on
this topic are explained in “Not all objects are instances of Object”
(§31.7.3).
The ECMAScript specification only knows a total of eight types. The names
of those types are (I’m using TypeScript’s names, not the spec’s names):
object the type of all objects (different from Object, the type of all
In contrast to Java (that inspired JavaScript here), primitive values are not
subtle. In a nutshell:
They are passed by identity (my term): when objects are assigned
are copied.
Other than that, primitive values and objects are quite similar: they both
have properties (key-value entries) and can be used in the same locations.
const x = 123;
const y = x;
// `y` is the same as any other number 123
assert.equal(y, 123);
passing by reference
JavaScript).
To see what’s so special about this way of comparing, read on and find out
14.4.2 Objects
Objects are covered in detail in “Objects” (§30) and the following chapter.
Object literal:
const obj = {
first: 'Jane',
last: 'Doe',
};
The object literal starts and ends with curly braces {}. It creates an
object with two properties. The first property has the key 'first' (a
string) and the value 'Jane'. The second property has the key 'last' and
the value 'Doe'. For more information on object literals, consult “Object
Array literal:
const fruits = ['strawberry', 'apple'];
The Array literal starts and ends with square brackets []. It creates an
Array with two elements: 'strawberry' and 'apple'. For more information
By default, you can freely change, add, and remove the properties of
objects:
object’s actual data on the heap (think shared main memory of a JavaScript
engine).
function, its identity is copied. Each object literal creates a fresh object on
the heap and returns its identity.
Now the old value { prop: 'value' } of obj is garbage (not used anymore).
“passing by sharing”.
14.4.2.3 Objects are compared by identity
Objects are compared by identity (my term): two variables are only equal if
they contain the same object identity. They are not equal if they refer to
The two operators typeof and instanceof let you determine what type a given
value x has:
objects
14.5.1 typeof
x typeof x
undefined 'undefined'
null 'object'
Boolean 'boolean'
Number 'number'
Bigint 'bigint'
String 'string'
Symbol 'symbol'
Function 'function'
Table 14.1 lists all results of typeof. They roughly correspond to the 7 types
of the language specification. Alas, there are two differences, and they are
language quirks:
typeof null returns 'object' and not 'null'. That’s a bug. Unfortunately,
it can’t be fixed. TC39 tried to do that, but it broke too much code on
the web.
exercises/values/typeof_exrc.mjs
Bonus: exercises/values/is_object_test.mjs
14.5.2 instanceof
This operator answers the question: has a value x been created by a class C?
x instanceof C
For example:
Exercise: instanceof
exercises/values/instanceof_exrc.mjs
functions that return “instances” of themselves if you invoke them via the
new operator.
ES6 introduced classes, which are mainly better syntax for constructor
functions.
In this book, I’m using the terms constructor function and class
interchangeably.
Classes can be seen as partitioning the single type object of the specification
into subtypes – they give us more types than the limited 7 ones of the
specification. Each class is the type of the objects that were created by it.
Each primitive type (except for the spec-internal types for undefined and null)
assert.equal(Number('123'), 123);
method .toString():
assert.equal((123).toString,
Number.prototype.toString);
for example:
assert.equal(Number.isInteger(123), true);
Lastly, you can also use Number as a class and create number objects.
These objects are different from real numbers and should be avoided.
The constructor functions related to primitive types are also called wrapper
types because they provide the canonical way of converting primitive values
There are two ways in which values are converted to other types in
JavaScript:
Explicit conversion: via functions such as String().
that type:
> Boolean(0)
false
> Number('123')
123
> String(123)
'123'
The following table describes in more detail how this conversion works:
x Object(x)
undefined {}
null {}
object x
its parameter to a string before parsing it. That explains the following result:
> Number.parseInt(123.45)
123
Parsing stops before the first non-digit character, which is why the result is
123.
Exercise: Converting values to primitives
exercises/values/conversion_exrc.mjs
15 Operators
JavaScript’s operators may seem quirky. With the following two rules, they
If an operator gets operands that don’t have the proper types, it rarely throws
First, the multiplication operator can only work with numbers. Therefore, it
object can only handle strings and symbols. All other values are coerced to
string:
> String([1,2,3])
'1,2,3'
> String([4,5,6])
'4,5,6'
Number mode means that if neither operand is a string (or an object that
Number(true) is 1.
assignment:
const x = value;
let y = value;
[ES1]
Arithmetic assignment operators: += -= *= /= %=
+= can also be used for string concatenation
[ES2016]
Introduced later: **=
[ES1]
Bitwise assignment operators: &= ^= |=
[ES1]
Bitwise shift assignment operators: <<= >>= >>>=
[ES2021]
Logical assignment operators: ||= &&= ??=
[ES2021]
15.3.2.1 Logical assignment operators
assignment operators:
a ||= b a || (a = b) Falsy
a ??= b a ?? (a = b) Nullish
a || (a = b)
a = a || b
performs an assignment.
For more on ??=, see “The nullish coalescing assignment operator (??=)”
(§16.4.5).
equivalent:
If, for example, op is +, then we get the operator += that works as follows.
assert.equal(str, '<b>Hello!</b>');
JavaScript has two kinds of equality operators: loose equality (==) and strict
> '' == 0
true
Objects are coerced to primitives if (and only if!) the other operand is
primitive:
If both operands are objects, they are only equal if they are the same object:
Strict equality never coerces. Two values are only equal if they have the
same type. Let’s revisit our previous interaction with the == operator and see
An object is only equal to another value if that value is the same object:
The === operator does not consider undefined and null to be equal:
I recommend to always use ===. It makes your code easier to understand and
Let’s look at two use cases for == and what I recommend to do instead.
single comparison:
if (x == 123) {
// x is either 123 or '123'
}
You can also convert x to a number when you first encounter it.
if (x == null) {
// x is either null or undefined
}
The problem with this code is that you can’t be sure if someone meant to
write it that way or if they made a typo and meant === null.
if (x != null) ···
if (x !== undefined && x !== null) ···
if (x) ···
15.4.4 Even stricter than ===: Object.is()
It is even stricter than ===. For example, it considers NaN, the error value for
Operator name
JavaScript’s ordering operators (table 15.1) work for both numbers and
strings:
> 5 >= 2
true
> 'bar' < 'foo'
true
The ordering operators don’t work well for comparing text in a human
The next two subsections discuss two operators that are rarely used.
The comma operator has two operands, evaluates both of them and returns
> void (3 + 2)
undefined
[ES2020]
[ES2021]
16.4.5 The nullish coalescing assignment operator (??=)
that a variable does not currently point to an object – for example, when it
distinction:
null means “the intentional absence of any object value” (a quote from
uninitialized, etc.).
null means “explicitly switched off”. That is, it helps implement a type
that comprises both meaningful values and a meta-value that stands for
“no meaningful value”. Such a type is called option type or maybe type
in functional programming.
The following subsections describe where undefined and null appear in the
let myVar;
assert.equal(myVar, undefined);
Parameter x is not provided:
function func(x) {
return x;
}
assert.equal(func(), undefined);
function func() {}
assert.equal(func(), undefined);
> Object.getPrototypeOf(Object.prototype)
null
we either get an object with matching data (if matching was successful) or
The JSON data format does not support undefined, only null:
Truthy means “is true if coerced to boolean”. Falsy means “is false if
Sometimes we receive a value and only want to use it if it isn’t either null or
a ?? b
a !== undefined && a !== null ? a : b
assert.equal(
countMatches(/a/g, 'ababa'), 3);
assert.equal(
countMatches(/b/g, 'ababa'), 2);
assert.equal(
countMatches(/x/g, 'ababa'), 0);
If there are one or more matches for regex inside str, then .match() returns an
Array. If there are no matches, it unfortunately returns null (and not the
return matchResult?.length ?? 0;
function getTitle(fileDesc) {
return fileDesc.title ?? '(Untitled)';
}
const files = [
{path: 'index.html', title: 'Home'},
{path: 'tmp.html'},
];
assert.deepEqual(
files.map(f => getTitle(f)),
['Home', '(Untitled)']);
In some cases, destructuring can also be used for default values – for
example:
function getTitle(fileDesc) {
const {title = '(Untitled)'} = fileDesc;
return title;
}
But it also returns the default for all other falsy values – for example:
[ES2021]
16.4.5 The nullish coalescing assignment operator (??=)
roughly equivalent:
a ??= b
a ?? (a = b)
undefined or null.
const books = [
{
isbn: '123',
},
{
title: 'ECMAScript Language Specification',
isbn: '456',
},
];
assert.deepEqual(
books,
[
{
isbn: '123',
title: '(Untitled)',
},
{
title: 'ECMAScript Language Specification',
isbn: '456',
},
]);
undefined and null are the only two JavaScript values where we get an
the following function, which reads (“gets”) property .foo and returns the
result.
function getFoo(x) {
return x.foo;
}
If we apply getFoo() to various values, we can see that it only fails for
> getFoo(undefined)
TypeError: Cannot read properties of undefined
(reading 'foo')
> getFoo(null)
TypeError: Cannot read properties of null (reading
'foo')
> getFoo(true)
undefined
> getFoo({})
undefined
Each primitive type has its own initialization value. For example, int
In JavaScript, each variable can hold both object values and primitive
17.5.1 Value-preservation
17.5.2 Short-circuiting
The primitive type boolean comprises two values – false and true:
These are three ways in which you can convert an arbitrary value x to a
boolean.
Boolean(x)
x ? true : false
!!x
Uses the logical Not operator (!). This operator coerces its operand to
x Boolean(x)
undefined false
null false
bigint 0 → false
symbol true
loop, JavaScript works differently than you may expect. Take, for example,
if (value) {}
Each value is either truthy or falsy. Consulting table 17.1, we can make an
undefined
null
Boolean: false
Numbers: 0, NaN
Bigint: 0n
String: ''
> Boolean('abc')
true
> Boolean([])
true
> Boolean({})
true
if (x) {
// x is truthy
}
if (!x) {
// x is falsy
}
if (x) {
// x is truthy
} else {
// x is falsy
}
The conditional operator that is used in the last line, is explained later in this
chapter.
Exercise: Truthiness
exercises/booleans/truthiness_exrc.mjs
For example, the following code checks if object obj has the property .prop:
if (obj.prop !== undefined) {
// obj has property .prop
}
if (obj.prop) {
// obj has property .prop
}
Truthiness-based existence checks have one pitfall: they are not very
if (obj.prop) {
// obj has property .prop
}
obj.prop is undefined.
In practice, this rarely causes problems, but you have to be aware of this
pitfall.
17.3.2 Use case: was a parameter provided?
provided a parameter:
function func(x) {
if (!x) {
throw new Error('Missing parameter x');
}
// ···
}
On the plus side, this pattern is established and short. It correctly throws
On the minus side, there is the previously mentioned pitfall: the code also
if (x === undefined) {
throw new Error('Missing parameter x');
}
function readFile(fileDesc) {
if (!fileDesc.path) {
throw new Error('Missing property: .path');
}
// ···
}
readFile({ path: 'foo.txt' }); // no error
This pattern is also established and has the usual caveat: it not only throws if
the property is missing, but also if it exists and has any of the falsy values.
If you truly want to check if the property exists, you have to use the in
operator:
if (! ('path' in fileDesc)) {
throw new Error('Missing property: .path');
}
syntax is:
It is evaluated as follows:
The conditional operator is also called ternary operator because it has three
operands.
Examples:
The following code demonstrates that whichever of the two branches “then”
and “else” is chosen via the condition, only that branch is evaluated. The
Output:
then
The binary logical operators && and || are value-preserving and short-
circuiting.
17.5.1 Value-preservation
returned unchanged:
> 12 || 'hello'
12
> 0 || 'hello'
'hello'
17.5.2 Short-circuiting
then the second operand is not evaluated. The only other operator that
For example, logical And (&&) does not evaluate its second operand if the
Output:
hello
1. Evaluate a.
2. Is the result falsy? Return it.
a && b
!a ? a : b
Examples:
1. Evaluate a.
a || b
a ? a : b
Examples:
17.5.4.1 Legacy use case for logical Or ( ||): providing default values
default values. Before that, logical Or was used for this purpose:
exercises/booleans/default_via_or_exrc.mjs
1. Evaluate x.
Examples:
> !false
true
> !true
false
> !0
true
> !123
false
> !''
true
> !'abc'
false
18 Numbers
18.1 Numbers are used for both floating point numbers and
integers
[ES2021]
18.2.4 Underscores (_) as separators in number literals
18.7 (Advanced)
notation
18.11.4 Number.prototype.*
18.11.5 Sources
Numbers are 64-bit floating point numbers and are also used for
This chapter covers numbers. Bigints are covered later in this book.
18.1 Numbers are used for both floating point numbers and
integers
The type number is used for both integers and floating point numbers in
JavaScript:
98
123.45
However, all numbers are doubles, 64-bit floating point numbers
(IEEE 754).
fraction:
Note that, under the hood, most JavaScript engines are often able to use real
// Binary (base 2)
assert.equal(0b11, 3); // ES6
// Octal (base 8)
assert.equal(0o10, 8); // ES6
Fractions:
> 35.0
35
N
Exponent: eN means ×10
> 3e2
300
> 3e-2
0.03
> 0.3e2
30
decimal dot:
[ES2021]
18.2.4 Underscores (_) as separators in number literals
Grouping digits to make long numbers more readable has a long tradition.
For example:
We can only put underscores between two digits. Therefore, all of the
3_.141
3._141
1_e12
1e_12
0_b111111000
0b_111111000
Number()
Number.parseInt()
Number.parseFloat()
For example:
> Number('123_456')
NaN
> Number.parseInt('123_456')
123
The rationale is that numeric separators are for code. Other kinds of input
n + m Addition ES1 3 + 4 → 7
n - m Subtraction ES1 9 - 1 → 8
n % m Remainder ES1 8 % 5 → 3
-8 % 5 → -3
n ** m Exponentiation ES2016 4 ** 2 → 16
Table 18.1: Binary arithmetic operators.
% is a remainder operator, not a modulo operator. Its result has the sign of
> 5 % 3
2
> -5 % 3
-2
For more information on the difference between remainder and modulo, see
the blog post “Remainder operator vs. modulo operator (with JavaScript
code)” on 2ality.
Table 18.2 summarizes the two operators unary plus (+) and negation (-).
Table 18.2: The operators unary plus (+) and negation (-).
The decrementation operator -- works the same, but subtracts one from its
operand. The next two examples explain the difference between the prefix
Prefix ++ and prefix -- change their operands and then return them.
let foo = 3;
assert.equal(++foo, 4);
assert.equal(foo, 4);
let bar = 3;
assert.equal(--bar, 2);
assert.equal(bar, 2);
Suffix ++ and suffix -- return their operands and then change them.
let foo = 3;
assert.equal(foo++, 3);
assert.equal(foo, 4);
let bar = 3;
assert.equal(bar--, 3);
assert.equal(bar, 2);
const arr = [ 4 ];
arr[0]++;
assert.deepEqual(arr, [5]);
exercises/numbers/is_odd_test.mjs
Number(value)
+value
it works.
x Number(x)
undefined NaN
x Number(x)
null 0
string '' → 0
whitespace
Examples:
assert.equal(Number(123.45), 123.45);
assert.equal(Number(''), 0);
assert.equal(Number('\n 123.45 \t'), 123.45);
assert.equal(Number('xyz'), NaN);
assert.equal(Number(-123n), -123);
overriding .valueOf():
> Number({ valueOf() { return 123 } })
123
exercises/numbers/parse_number_test.mjs
NaN
Infinity
to be a number:
> Number('$$$')
NaN
> Number(undefined)
NaN
> Math.log(-1)
NaN
> Math.sqrt(-1)
NaN
> NaN - 3
NaN
> 7 ** NaN
NaN
NaN is the only JavaScript value that is not strictly equal to itself:
const n = NaN;
assert.equal(n === n, false);
const x = NaN;
> [NaN].indexOf(NaN)
-1
Others can:
> [NaN].includes(NaN)
true
> [NaN].findIndex(x => Number.isNaN(x))
0
> [NaN].find(x => Number.isNaN(x))
NaN
Alas, there is no simple rule of thumb. We have to check for each method
> 5 / 0
Infinity
> -5 / 0
-Infinity
Infinity is larger than all other numbers (except NaN), making it a good
default value:
function findMinimum(numbers) {
let min = Infinity;
for (const n of numbers) {
if (n < min) min = n;
}
return min;
}
const x = Infinity;
exercises/numbers/find_max_test.mjs
(according to the IEEE 754 standard). That means that decimal fractions
52
Fraction 52 bits [0, 2 −1]
follows:
sign exponent
(–1) × 0b1.fraction × 2
exponent
mantissa × 10
Let’s try out this representation for a few floating point numbers.
To encode the integer 123, we use the mantissa 123 and multiply it
0
with 1 (10 ):
To encode the integer −45, we use the mantissa −45 and, again, the
exponent zero:
We use the negative exponent −1 to move that point one digit to the
left:
For the number 0.25, we move the point two digits to the left:
exponent ≥ 1.
For example:
These fractions help with understanding why there are numbers that our
10 in the denominator.
1/2 can be represented as 5/10. We turned the 2 in the denominator into
25.
already a power of 2.
already a power of 2.
converted to a power of 2.
converted to a power of 2.
Now we can see why 0.1 + 0.2 doesn’t produce a correct result: internally,
proposal “Decimal”. Until that happens, we can use libraries such as big.js.
fractions:
In this section, we’ll look at a few tools for working with these pseudo-
> Math.floor(2.1)
2
> Math.floor(2.9)
2
> Math.ceil(2.1)
3
> Math.ceil(2.9)
3
> Math.round(2.4)
2
> Math.round(2.5)
3
Math.trunc(n): removes any decimal fraction (after the point) that n has,
> Math.trunc(2.1)
2
> Math.trunc(2.9)
2
53 53
Range: (−2 , 2 )
Array indices
32
Range: [0, 2 −1) (excluding the maximum length)
Precision: 32 bits
32
Range of unsigned right shift (>>>): unsigned, [0, 2 )
31 31
Range of all other bitwise operators: signed, [−2 , 2 )
This is the range of integer numbers that are safe in JavaScript (53 bits plus
a sign):
53 53
[–(2 )+1, 2 –1]
the power of an exponent, higher integers can also be represented, but then
54
For example (18014398509481984 is 2 ):
> 18014398509481984
18014398509481984
> 18014398509481985
18014398509481984
> 18014398509481986
18014398509481984
> 18014398509481987
18014398509481988
assert.equal(Number.MAX_SAFE_INTEGER, (2 ** 53) -
1);
assert.equal(Number.MIN_SAFE_INTEGER, -
Number.MAX_SAFE_INTEGER);
assert.equal(Number.isSafeInteger(5), true);
assert.equal(Number.isSafeInteger('5'), false);
assert.equal(Number.isSafeInteger(5.1), false);
assert.equal(Number.isSafeInteger(Number.MAX_SAFE_IN
TEGER), true);
assert.equal(Number.isSafeInteger(Number.MAX_SAFE_IN
TEGER+1), false);
exercises/numbers/is_safe_integer_test.mjs
18.9.3.1 Safe computations
The following result is incorrect and unsafe, even though both of its
> 9007199254740990 + 3
9007199254740992
The following result is safe, but incorrect. The first operand is unsafe; the
> 9007199254740995 - 10
9007199254740986
integers.
For each bitwise operator, this book mentions the types of its operands and
31 31
Int32 signed 32-bit integer 32 bits incl. sign [−2 , 2 )
32
Uint32 unsigned 32-bit integer 32 bits [0, 2 )
“computation”) and that Int32 and Uint32 only affect how JavaScript
numbers are converted to and from integers (steps “input” and “output”).
Type
Operation Name
signature
complement
The bitwise Not operator (table 18.5) inverts each binary digit of its
operand:
> b32(~0b100)
'11111111111111111111111111111011'
> 4 + ~4
-1
> -11 + ~-11
-1
The binary bitwise operators (table 18.6) combine the bits of their operands
num >> count Signed right shift Int32 × Uint32 → Int32 ES1
The shift operators (table 18.7) move binary digits to the left or to the right:
implementation of it:
/**
* Return a string representing n as a 32-bit
unsigned integer,
* in binary notation.
*/
function b32(n) {
// >>> ensures highest bit isn’t interpreted as a
sign
return (n >>> 0).toString(2).padStart(32, '0');
}
assert.equal(
b32(6),
'00000000000000000000000000000110');
n >>> 0 means that we are shifting n zero bits to the right. Therefore, in
principle, the >>> operator does nothing, but it still coerces n to an unsigned
32-bit integer:
> 12 >>> 0
12
> -12 >>> 0
4294967284
> (2**32 + 1) >>> 0
1
isFinite()
isNaN()
parseFloat()
parseInt()
[ES6]
Number.EPSILON
-16
Approximately: 2.2204460492503130808472633361816 × 10
[ES6]
Number.MAX_SAFE_INTEGER
53
(2 −1).
[ES1]
Number.MAX_VALUE
308
Approximately: 1.7976931348623157 × 10
[ES6]
Number.MIN_SAFE_INTEGER
53
(−2 +1).
[ES1]
Number.MIN_VALUE
−324
The smallest positive JavaScript number. Approximately 5 × 10 .
[ES1]
Number.NaN
[ES1]
Number.NEGATIVE_INFINITY
[ES1]
Number.POSITIVE_INFINITY
[ES6]
Number.isFinite(num)
Returns true if num is an actual number (neither Infinity nor -Infinity nor
NaN).
> Number.isFinite(Infinity)
false
> Number.isFinite(-Infinity)
false
> Number.isFinite(NaN)
false
> Number.isFinite(123)
true
[ES6]
Number.isInteger(num)
Returns true if num is a number and does not have a decimal fraction.
> Number.isInteger(-17)
true
> Number.isInteger(33)
true
> Number.isInteger(33.1)
false
> Number.isInteger('33')
false
> Number.isInteger(NaN)
false
> Number.isInteger(Infinity)
false
[ES6]
Number.isNaN(num)
> Number.isNaN(NaN)
true
> Number.isNaN(123)
false
> Number.isNaN('abc')
false
[ES6]
Number.isSafeInteger(num)
integer.
[ES6]
Number.parseFloat(str)
[ES6]
Number.parseInt(str, radix=10)
number to an integer:
18.11.4 Number.prototype.*
[ES3]
Number.prototype.toExponential(fractionDigits?)
notation.
> 1234..toString()
'1234'
.toString().
> 0.003.toString()
'0.003'
> 0.003.toExponential()
'3e-3'
[ES3]
Number.prototype.toFixed(fractionDigits=0)
to fractionDigits digits.
> 1234..toPrecision(4)
'1234'
> 1234..toPrecision(5)
'1234.0'
> 1.234.toPrecision(3)
'1.23'
[ES1]
Number.prototype.toString(radix=10)
Returns a string representation of the number.
> 123.456.toString()
'123.456'
radix:
> 1234567890..toString(36)
'kf12oi'
number.
Wikipedia
19.3 Rounding
19.6 Sources
Math is an object with data properties and methods for processing numbers.
You can see it as a poor man’s module: It was created long before JavaScript
had modules.
[ES1]
Math.E: number
2.7182818284590452354.
[ES1]
Math.LN10: number
[ES1]
Math.LN2: number
[ES1]
Math.LOG2E: number
[ES1]
Math.PI: number
[ES1]
Math.SQRT1_2: number
[ES1]
Math.SQRT2: number
[ES6]
Math.cbrt(x: number): number
> Math.cbrt(8)
2
[ES1]
Math.exp(x: number): number
x
Returns e (e being Euler’s number). The inverse of Math.log().
> Math.exp(0)
1
> Math.exp(1) === Math.E
true
[ES6]
Math.expm1(x: number): number
[ES1]
Math.log(x: number): number
inverse of Math.exp().
> Math.log(1)
0
> Math.log(Math.E)
1
> Math.log(Math.E ** 2)
2
[ES6]
Math.log1p(x: number): number
[ES6]
Math.log10(x: number): number
> Math.log10(1)
0
> Math.log10(10)
1
> Math.log10(100)
2
[ES6]
Math.log2(x: number): number
> Math.log2(1)
0
> Math.log2(2)
1
> Math.log2(4)
2
[ES1]
Math.pow(x: number, y: number): number
y
Returns x , x to the power of y. The same as x ** y.
> Math.pow(2, 3)
8
> Math.pow(25, 0.5)
5
[ES1]
Math.sqrt(x: number): number
> Math.sqrt(9)
3
19.3 Rounding
approaches to rounding.
[ES1]
Math.ceil(x: number): number
> Math.ceil(2.1)
3
> Math.ceil(2.9)
3
[ES1]
Math.floor(x: number): number
> Math.round(2.4)
2
> Math.round(2.5)
3
[ES6]
Math.trunc(x: number): number
> Math.trunc(2.1)
2
> Math.trunc(2.9)
2
Table 19.1 shows the results of the rounding functions for a few
representative inputs.
Math.floor -3 -3 -3 2 2 2
-2.9 -2.5 -2.1 2.1 2.5 2.9
Math.ceil -2 -2 -2 3 3 3
Math.round -3 -2 -2 2 3 3
Math.trunc -2 -2 -2 2 2 2
Table 19.1: Rounding functions of Math. Note how things change with
infinity”.
All angles are specified in radians. Use the following two functions to
function degreesToRadians(degrees) {
return degrees / 180 * Math.PI;
}
assert.equal(degreesToRadians(90), Math.PI/2);
function radiansToDegrees(radians) {
return radians / Math.PI * 180;
}
assert.equal(radiansToDegrees(Math.PI), 180);
[ES1]
Math.acos(x: number): number
[ES1]
Math.asin(x: number): number
> Math.asin(0)
0
> Math.asin(1)
1.5707963267948966
[ES6]
Math.asinh(x: number): number
[ES1]
Math.atan(x: number): number
[ES6]
Math.atanh(x: number): number
[ES1]
Math.atan2(y: number, x: number): number
Returns the arc tangent of the quotient y/x.
[ES1]
Math.cos(x: number): number
> Math.cos(0)
1
> Math.cos(Math.PI)
-1
[ES6]
Math.cosh(x: number): number
[ES6]
Math.hypot(...values: Array<number>): number
Returns the square root of the sum of the squares of values (Pythagoras’
theorem):
> Math.hypot(3, 4)
5
[ES1]
Math.sin(x: number): number
> Math.sin(0)
0
> Math.sin(Math.PI / 2)
1
[ES6]
Math.sinh(x: number): number
[ES1]
Math.tan(x: number): number
> Math.tan(0)
0
> Math.tan(1)
1.5574077246549023
[ES6]
Math.tanh(x: number): number;
[ES1]
Math.abs(x: number): number
> Math.abs(3)
3
> Math.abs(-3)
3
> Math.abs(0)
0
[ES6]
Math.clz32(x: number): number
Counts the leading zero bits in the 32-bit integer x. Used in DSP
algorithms.
> Math.clz32(0b01000000000000000000000000000000)
1
> Math.clz32(0b00100000000000000000000000000000)
2
> Math.clz32(2)
30
> Math.clz32(1)
31
[ES1]
Math.max(...values: Array<number>): number
[ES1]
Math.min(...values: Array<number>): number
> Math.sign(-8)
-1
> Math.sign(0)
0
> Math.sign(3)
1
19.6 Sources
Wikipedia
[ES2020]
(advanced)
20.2 Bigints
[ES2021]
20.3.1 Underscores (_) as separators in bigint literals
bigints?
There only was a single type for floating point numbers and integers:
representation is called small integer and usually fits into 32 bits. For
31 31
engine is from −2 to 2 −1 (source).
integers” (§18.9.3).
disappear.
20.2 Bigints
Bigint is a new primitive data type for integers. Bigints don’t have a fixed
storage size in bits; their sizes adapt to the integers they represent:
Small integers are represented with fewer bits than large integers.
There is no negative lower limit or positive upper limit for the integers
example:
123n
Bigints are primitive values. typeof returns a new result for them:
53
As a consequence, if we go beyond the highest safe integer 2 −1, there are
still some integers that can be represented, but with gaps between them:
This is what using bigints looks like (code based on an example in the
proposal):
/**
* Takes a bigint as an argument and returns a
bigint
*/
function nthPrime(nth) {
if (typeof nth !== 'bigint') {
throw new TypeError();
}
function isPrime(p) {
for (let i = 2n; i < p; i++) {
if (p % i === 0n) return false;
}
return true;
}
for (let i = 2n; ; i++) {
if (isPrime(i)) {
if (--nth === 0n) return i;
}
}
}
assert.deepEqual(
[1n, 2n, 3n, 4n, 5n].map(nth => nthPrime(nth)),
[2n, 3n, 5n, 7n, 11n]
);
Decimal: 123n
Hexadecimal: 0xFFn
Binary: 0b1101n
Octal: 0o777n
Negative bigints are produced by prefixing the unary minus operator: -0123n
[ES2021]
20.3.1 Underscores (_) as separators in bigint literals
bigint literals:
const massOfEarthInKg =
6_000_000_000_000_000_000_000_000n;
Bigints are often used to represent money in the financial technical sector.
With most operators, we are not allowed to mix bigints and numbers. If we
> 2n + 1
TypeError: Cannot mix BigInt and other types, use
explicit conversions
The reason for this rule is that there is no general way of coercing a number
9007199254740993n or 9007199254740992?
2**53 + 1n
It is also not clear what the result of the following expression should be:
2n**53n * 3.3
> 7n * 3n
21n
> 1n / 2n
0n
> -(-64n)
64n
Unary + is not supported for bigints because much code relies on it coercing
> +23n
TypeError: Cannot convert a BigInt value to a number
Comparing bigints and numbers does not pose any risks. Therefore, we can
> 3n > -1
true
either unsigned or signed. If they are signed, the negative of an integer is its
Due to these integers having a fixed size, their highest bits indicate their
signs:
-2 is ···111110
-3 is ···111101
-4 is ···111100
That is, a negative sign is more of an external flag and not represented as an
actual bit.
20.4.3.3 ~
Bitwise Not ( )
> ~0b10n
-3n
> ~0n
-1n
> ~-2n
1n
20.4.3.4 & |, ^)
Binary bitwise operators ( ,
them to numbers:
> (0b1010n | 0b0111n).toString(2)
'1111'
> (0b1010n & 0b0111n).toString(2)
'10'
The signed shift operators for bigints preserve the sign of a number:
> 2n << 1n
4n
> -2n << 1n
-4n
> 2n >> 1n
1n
> -2n >> 1n
-1n
Recall that -1n is a sequence of ones that extends infinitely to the left. That’s
> 2n >>> 1n
TypeError: BigInts have no unsigned right shift, use
>> instead
Why? The idea behind unsigned right shifting is that a zero is shifted in
“from the left”. In other words, the assumption is that there is a finite
However, with bigints, there is no “left”, their binary digits extend infinitely.
Signed right shift works even with an infinite number of digits because the
> 0n == false
true
> 1n == true
true
Strict equality (===) and inequality (!==) only consider values to be equal if
BigInt.
x BigInt(x)
> BigInt(undefined)
TypeError: Cannot convert undefined to a BigInt
> BigInt(null)
TypeError: Cannot convert null to a BigInt
> BigInt('abc')
SyntaxError: Cannot convert abc to a BigInt
> BigInt('123n')
SyntaxError: Cannot convert 123n to a BigInt
> BigInt('123')
123n
> BigInt('0xFF')
255n
> BigInt('0b1101')
13n
> BigInt('0o777')
511n
> BigInt(123.45)
RangeError: The number 123.45 cannot be converted to
a BigInt because
it is not an integer
> BigInt(123)
123n
20.5.1.4 Converting objects
overriding .valueOf():
BigInt.prototype.toLocaleString(locales?, options?)
BigInt.prototype.toString(radix?)
BigInt.prototype.valueOf()
BigInt.asIntN(width, theInt)
Casts theInt to width bits (signed). This influences how the value is
represented internally.
BigInt.asUintN(width, theInt)
This table show what happens if we convert bigints to other primitive types:
(example)
Footnote:
(1) Unary + is not supported for bigints, because much code relies on it
Thanks to bigints, Typed Arrays and DataViews can support 64-bit values:
BigInt64Array
BigUint64Array
DataView methods:
DataView.prototype.getBigInt64()
DataView.prototype.setBigInt64()
DataView.prototype.getBigUint64()
DataView.prototype.setBigUint64()
The JSON standard is fixed and won’t change. The upside is that old JSON
parsing code will never be outdated. The downside is that JSON can’t be
> JSON.stringify(123n)
TypeError: Do not know how to serialize a BigInt
> JSON.stringify([123n])
TypeError: Do not know how to serialize a BigInt
The following code shows how to parse strings such as the one that we have
20.9.1 How do I decide when to use numbers and when to use bigints?
My recommendations:
Use numbers for up to 53 bits and for Array indices. Rationale: They
Array.prototype.forEach()
Array.prototype.entries()
Use bigints for large numeric values: If your fraction-less values don’t
All existing web APIs return and accept only numbers and will only
20.9.2 Why not just increase the precision of numbers in the same
One could conceivably split number into integer and double, but that would add
Acknowledgements:
Thanks to Daniel Ehrenberg for reviewing an earlier version of this
content.
content.
21 Unicode – a brief introduction
(advanced)
world’s writing systems. Virtually all modern software that works with text,
A new version of the standard is published every year (with new emojis,
Code points are numbers that represent the atomic parts of Unicode
text. Most of them represent visible symbols but they can also have
other meanings such as specifying an aspect of a symbol (the accent of
Code units are numbers that encode code points, to store or transmit
Unicode text. One or more code units encode a single code point. Each
code unit has the same size, which depends on the encoding format
that is used. The most popular format, UTF-8, has 8-bit code units.
The first version of Unicode had 16-bit code points. Since then, the number
of characters has grown considerably and the size of code points was
each:
0xEFFFF
0x0F0000–0x10FFFF
> 'A'.codePointAt(0).toString(16)
'41'
> 'ü'.codePointAt(0).toString(16)
'fc'
> 'π'.codePointAt(0).toString(16)
🙂
'3c0'
> ' '.codePointAt(0).toString(16)
'1f642'
The hexadecimal numbers of the code points tell us that the first three
characters reside in plane 0 (within 16 bits), while the emoji resides in plane
1.
The main ways of encoding code points are three Unicode Transformation
Formats (UTFs): UTF-32, UTF-16, UTF-8. The number at the end of each
point. This format is the only one with fixed-length encoding; all others use
Astral planes: The BMP comprises 0x10_000 code points. Given that
the remaining 0x100_000 code points (20 bits). The BMP has two
0xD800-0xDBFF
0xDC00-0xDFFF
A leading surrogate
A trailing surrogate
surrogate.
This is how the bits of the code points are distributed among the surrogates:
0bhhhhhhhhhhllllllllll // code point - 0x10000
0b110110hhhhhhhhhh // 0xD800 + 0bhhhhhhhhhh
0b110111llllllllll // 0xDC00 + 0bllllllllll
🙂
> ' '.codePointAt(0).toString(16)
🙂
'1f642'
> ' '.length
🙂
2
> ' '.split('')
[ '\uD83D', '\uDE42' ]
In contrast, code point 0x03C0 (π) is part of the BMP and therefore
> 'π'.length
1
1FFFFF bits)
Notes:
will follow?
software.
Three examples:
A 0x0041 01000001
The Unicode encoding formats that are used in web development are: UTF-
16 and UTF-8.
16.
For more information on Unicode and strings, consult “Atoms of text: code
HTML and JavaScript are almost always encoded as UTF-8 these days.
For HTML modules loaded in web browsers, the standard encoding is also
UTF-8.
the various writing systems of the world. That’s why there are several
different Unicode terms that all mean “character” in some way: code point,
grapheme cluster.
Array.from() to split a string into an Array with code points (for details,
stored in a font. More than one glyph may be used to draw a single
symbol – for example, the symbol “é” may be drawn by combining the
The distinction between a concept and its representation is subtle and can
clusters
22.2.1 Escaping
clusters
Strings are primitive values in JavaScript and immutable. That is, string-
related operations always produce new strings and never change existing
strings.
\\ represents a backslash
\n represents a newline
\t represents a tab
normal characters:
assert.equal(
String.raw`\ \n\t`, // (A)
'\\ \\n\\t',
);
> String(undefined)
'undefined'
> String(null)
'null'
> String(123.45)
'123.45'
> String(true)
'true'
Concatenating strings:
assert.equal(
'I bought ' + 3 + ' apples',
'I bought 3 apples',
);
Code points are the atomic parts of Unicode text. Most of them fit into one
assert.equal(
'A'.length, 1
);
🙂
assert.equal(
' '.length, 2
);
This subsection gives a brief overview of the string API. There is a more
Finding substrings:
> 'abca'.includes('a')
true
> 'abca'.startsWith('ab')
true
> 'abca'.endsWith('ca')
true
> 'abca'.indexOf('a')
0
> 'abca'.lastIndexOf('a')
3
assert.deepEqual(
'a, b,c'.split(/, ?/),
['a', 'b', 'c']
);
assert.equal(
['a', 'b', 'c'].join(', '),
'a, b, c'
);
> '*'.repeat(5)
'*****'
> '= b2b ='.toUpperCase()
'= B2B ='
> 'ΑΒΓ'.toLowerCase()
'αβγ'
Plain string literals are delimited by either single quotes or double quotes:
Single quotes are used more often because it makes it easier to mention
String interpolation
Multiple lines
22.2.1 Escaping
Tab: '\t'
Backslash: '\\'
The backslash also lets us use the delimiter of a string literal inside that
literal:
assert.equal(
'She said: "Let\'s go!"',
"She said: \"Let's go!\"");
JavaScript has no extra data type for characters – characters are always
represented as strings.
The characters we see on screen are called grapheme clusters. Most of them
JavaScript characters:
If at least one operand is a string, the plus operator (+) converts any non-
by piece:
exercises/strings/concat_string_array_test.mjs
in line A):
String(x)
''+x
Examples:
assert.equal(String(undefined), 'undefined');
assert.equal(String(null), 'null');
assert.equal(String(false), 'false');
assert.equal(String(true), 'true');
assert.equal(String(123.45), '123.45');
> String(false)
'false'
> Boolean('false')
true
The only string for which Boolean() returns false, is the empty string.
Plain objects have a default string representation that is not very useful:
information:
> String([true])
'true'
> String(['true'])
'true'
> String(true)
'true'
const obj = {
toString() {
return 'hello';
}
};
assert.equal(String(obj), 'hello');
22.5.3 An alternate way of stringifying values
The caveat is that JSON only supports null, booleans, numbers, strings,
Arrays, and objects (which it always treats as if they were created by object
literals).
Tip: The third parameter lets us switch on multiline output and specify how
{
"first": "Jane",
"last": "Doe"
}
on the numeric values of JavaScript characters. That means that the order
that JavaScript uses for strings is different from the one used in dictionaries
Properly comparing text is beyond the scope of this book. It is supported via
grapheme clusters
Code points are the atomic parts of Unicode text. Each code point is 21
bits in size.
16. It uses one or two 16-bit code units to encode a single code point.
the JavaScript standard library, code units are also called char
codes.
The following code demonstrates that a single code point comprises one or
🙂
// 1 code point, 2 JavaScript characters:
assert.equal(' '.length, 2);
A Unicode code point escape lets us specify a code point hexadecimally (1–
escapes and Unicode code unit escapes (which we’ll encounter later)
characters:
🙂
> String.fromCodePoint(0x1F642)
' '
🙂
> ' '.codePointAt(0).toString(16)
'1f642'
We can iterate over a string, which visits code points (not JavaScript
Output:
🙂
a
🙂a')
🙂
> Array.from('
[ ' ', 'a' ]
🙂
> Array.from(' a').length
🙂
2
> ' a'.length
3
🙂
> '\uD83D\uDE42'
' '
And we can use String.fromCharCode(). Char code is the standard library’s
> String.fromCharCode(0xD83D) +
🙂
String.fromCharCode(0xDE42)
' '
🙂
> ' '.charCodeAt(0).toString(16)
'd83d'
If the code point of a character is below 256, we can refer to it via a ASCII
> 'He\x6C\x6Co'
'Hello'
When working with text that may be written in any human language, it’s
code points.
TC39 is working on Intl.Segmenter, a proposal for the ECMAScript
Until that proposal becomes a standard, we can use one of several libraries
x String(x)
undefined 'undefined'
null 'null'
[ES1]
Convert number to string: String.fromCharCode()
[ES1]
Convert string to number: string method .charCodeAt()
[ES6]
Convert number to string: String.fromCodePoint()
[ES6]
Convert string to number: string method .codePointAt()
[ES6]
String.prototype.startsWith(searchString, startPos=0)
> '.gitignore'.startsWith('.')
true
> 'abcde'.startsWith('bc', 1)
true
[ES6]
String.prototype.endsWith(searchString, endPos=this.length)
Returns true if the string would end with searchString if its length were
Returns true if the string contains the searchString and false otherwise.
> 'abc'.includes('b')
true
> 'abc'.includes('b', 2)
false
[ES1]
String.prototype.indexOf(searchString, minIndex=0)
Returns the lowest index at which searchString appears within the string
> 'abab'.indexOf('a')
0
> 'abab'.indexOf('a', 1)
2
> 'abab'.indexOf('c')
-1
[ES1]
String.prototype.lastIndexOf(searchString, maxIndex=Infinity)
Returns the highest index at which searchString appears within the
> 'abab'.lastIndexOf('ab', 2)
2
> 'abab'.lastIndexOf('ab', 1)
0
> 'abab'.lastIndexOf('ab')
2
[ES3]
String.prototype.match(regExpOrString)
match(
regExpOrString: string | RegExp
): null | RegExpMatchArray
.match() returns the first match for regExpOrString within the string.
mentioned steps.
Examples:
> 'ababb'.match(/a(b+)/)
{ 0: 'ab', 1: 'b', index: 0, input: 'ababb',
groups: undefined }
> 'ababb'.match(/a(?<foo>b+)/)
{ 0: 'ab', 1: 'b', index: 0, input: 'ababb',
groups: { foo: 'b' } }
> 'abab'.match(/x/)
null
match(
regExpOrString: RegExp
): null | Array<string>
If flag /g of regExpOrString is set, .match() returns either an Array
> 'ababb'.match(/a(b+)/g)
[ 'ab', 'abb' ]
> 'ababb'.match(/a(?<foo>b+)/g)
[ 'ab', 'abb' ]
> 'abab'.match(/x/g)
null
[ES3]
String.prototype.search(regExpOrString)
> 'a2b'.search(/[0-9]/)
1
> 'a2b'.search('[0-9]')
1
[ES3]
String.prototype.slice(start=0, end=this.length)
Returns the substring of the string that starts at (including) index start
> 'abc'.at(0)
'a'
> 'abc'.at(-1)
'c'
[ES3]
String.prototype.split(separator, limit?)
Splits the string into an Array of substrings – the strings that occur
Warning about .split(''): Using the method this way splits a string
into JavaScript characters. That doesn’t work well when dealing with
🙂🙂
> ' X '.split('')
[ '\uD83D', '\uDE42', 'X', '\uD83D', '\uDE42' ]
🙂X🙂')
🙂 🙂' ]
> Array.from('
[ ' ', 'X', '
[ES1]
String.prototype.substring(start, end=this.length)
[ES3]
String.prototype.concat(...strings)
Appends (fragments of) fillString to the string until it has the desired
any changes.
> '#'.padEnd(2)
'# '
> 'abc'.padEnd(2)
'abc'
> '#'.padEnd(5, 'abc')
'#abca'
[ES2017]
String.prototype.padStart(len, fillString=' ')
Prepends (fragments of) fillString to the string until it has the desired
any changes.
> '#'.padStart(2)
' #'
> 'abc'.padStart(2)
'abc'
> '#'.padStart(5, 'abc')
'abca#'
[ES6]
String.prototype.repeat(count=0)
> '*'.repeat()
''
> '*'.repeat(3)
'***'
[ES2021]
String.prototype.replaceAll(searchValue, replaceValue)
[ES3]
“str.replace(searchValue, replacementValue) ” (§45.13.8.1).
(1 of 2) replaceValue is string.
replaceAll(
searchValue: string | RegExp,
replaceValue: string
): string
$$: becomes $
for the string '$0', it does not refer to the complete match)
$&: becomes the complete match
Examples:
Example:
assert.equal(
'a 1995-12 b'.replaceAll(
/(?<year>[0-9]{4})-(?<month>[0-9]{2})/g,
'|$<month>|'),
'a |12| b');
(2 of 2) replaceValue is function.
replaceAll(
searchValue: string | RegExp,
replaceValue: (...args: Array<any>) =>
string
): string
(Etc.)
string?
[ES3]
replacementValue) ” (§45.13.8.1).
replace(
searchValue: string | RegExp,
replaceValue: string
): string
occurrence:
replace(
searchValue: string | RegExp,
replaceValue: (...args: Array<any>) =>
string
): string
[ES1]
String.prototype.toUpperCase()
characters are converted to uppercase. How well that works for various
> '-a2b-'.toUpperCase()
'-A2B-'
> 'αβγ'.toUpperCase()
'ΑΒΓ'
[ES1]
String.prototype.toLowerCase()
characters are converted to lowercase. How well that works for various
[ES2019]
String.prototype.trimStart()
[ES6]
String.prototype.normalize(form = 'NFC')
Normalizes the string according to the Unicode Normalization
Forms.
[ES2024]
String.prototype.isWellFormed()
🙂
> ' '.split('') // split into code units
[ '\uD83D', '\uDE42' ]
> '\uD83D\uDE42'.isWellFormed()
true
> '\uD83D\uDE42\uD83D'.isWellFormed() // lone
surrogate 0xD83D
false
[ES2024]
String.prototype.toWellFormed()
Each JavaScript string character is a UTF-16 code unit. One code point
is encoded as either one UTF-16 code unit or two UTF-16 code unit. In
the latter case, the two code units are called leading surrogate and
correct symbols.”
🙂
assert.deepEqual(
' '.split(''), // split into code units
['\uD83D', '\uDE42']
);
assert.deepEqual(
// 0xD83D is a lone surrogate
'\uD83D\uDE42\uD83D'.toWellFormed().split(''),
['\uD83D', '\uDE42', '\uFFFD']
);
exercises/strings/remove_extension_test.mjs
23 Using template literals and tagged
[ES6]
templates
23.6 (Advanced)
Before we dig into the two features template literal and tagged template,
web development and often defined via text files. For example, the
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
</div>
This template has two blanks to be filled in: title and body. It is used
like this:
leads to the function being called. Its arguments are derived from the
Note that getArgs() receives both the text of the literal and the data
A template literal has two new features compared to a normal string literal.
value inside a ${}, it is converted to a string and inserted into the string
function tagFunc(...args) {
return args;
}
assert.deepEqual(
tagFunc`Setting ${setting} is ${value}!`, // (A)
[['Setting ', ' is ', '!'], 'dark mode', true] //
(B)
);
The function tagFunc before the first backtick is called a tag function. Its
arguments are:
The static (fixed) parts of the literal (the template strings) are kept separate
So far, we have only seen the cooked interpretation of template strings. But
The raw interpretation enables raw string literals via String.raw (described
function cookedRaw(templateStrings,
...substitutions) {
return {
cooked: Array.from(templateStrings), // copy
only Array elements
raw: templateStrings.raw,
substitutions,
};
}
assert.deepEqual(
cookedRaw`\tab${'subst'}\newline\\`,
{
cooked: ['\tab', '\newline\\'],
raw: ['\\tab', '\\newline\\\\'],
substitutions: ['subst'],
});
We can also use Unicode code point escapes (\u{1F642}), Unicode code unit
If the syntax of one of these escapes isn’t correct, the corresponding cooked
assert.deepEqual(
cookedRaw`\uu\xx ${1} after`,
{
cooked: [undefined, ' after'],
raw: ['\\uu\\xx ', ' after'],
substitutions: [1],
});
Why was that changed? We can now use tagged templates for text that was
windowsPath`C:\uuu\xxx\111`
latex`\unicode`
Lit is a library for building web components that uses tagged templates for
HTML templating:
@customElement('my-element')
class MyElement extends LitElement {
// ···
render() {
return html`
<ul>
${repeat(
this.items,
(item) => item.id,
(item, index) => html`<li>${index}:
${item.name}</li>`
)}
</ul>
`;
}
}
repeat() is a custom function for looping. Its second parameter produces
unique keys for the values returned by the third parameter. Note the nested
The library “regex” by Steven Levithan provides template tags that help
Flag /v
Flag /x (emulated) enables insignificant whitespace and line comments
via #.
templates:
TypeScript, etc.
Raw string literals are implemented via the tag function String.raw. They are
characters, etc.):
assert.equal(String.raw`\back`, '\\back');
This helps whenever data contains backslashes – for example, strings with
regular expressions:
All three regular expressions are equivalent. With a normal string literal, we
have to write the backslash twice, to escape it for that literal. With a raw
Raw string literals are also useful for specifying Windows filename paths:
23.6 (Advanced)
If we put multiline text in template literals, two goals are in conflict: On one
hand, the template literal should be indented to fit inside the source code.
On the other hand, the lines of its content should start in the leftmost
column.
For example:
function div(text) {
return `
<div>
${text}
</div>
`;
}
console.log('Output:');
console.log(
div('Hello!')
// Replace spaces with mid-dots:
.replace(/ /g, '·')
// Replace \n with #\n:
.replace(/\n/g, '#\n')
);
Due to the indentation, the template literal fits well into the source code.
Alas, the output is also indented. And we don’t want the return at the
Output:
#
····<div>#
······Hello!#
····</div>#
··
There are two ways to fix this: via a tagged template or by trimming the
The first fix is to use a custom template tag that removes the unwanted
whitespace. It uses the first line after the initial line break to determine in
which column the text starts and shortens the indentation everywhere. It
also removes the line break at the very beginning and the indentation at the
Output:
<div>#
Hello!#
</div>
23.7.2 Fix: .trim()
function divDedented(text) {
return `
<div>
${text}
</div>
`.trim().replace(/\n/g, '#\n');
}
console.log('Output:');
console.log(divDedented('Hello!'));
beginning and at the end, but the content itself must start in the leftmost
column. The advantage of this solution is that we don’t need a custom tag
Output:
<div>#
Hello!#
</div>
obvious how to use them for (text) templating: A text template gets its data
from an object, while a template literal gets its data from variables. The
const addresses = [
{ first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
];
The function tmpl() that produces the HTML table looks as follows:
The first one (line 1) takes addrs, an Array with addresses, and returns a
The second one (line 4) takes addr, an object containing an address, and
returns a string with a table row. Note the .trim() at the end, which
The first templating function produces its result by wrapping a table element
around an Array that it joins into a string (line 10). That Array is produced
by mapping the second templating function to each element of addrs (line 3).
(line 6 and line 7). Its implementation is shown in the next subsection.
Let us call tmpl() with the addresses and log the result:
console.log(tmpl(addresses));
<table>
<tr>
<td><Jane></td>
<td>Bond</td>
</tr><tr>
<td>Lars</td>
<td><Croft></td>
</tr>
</table>
HTML:
function escapeHtml(str) {
return str
.replace(/&/g, '&') // first!
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/`/g, '`')
;
}
assert.equal(
escapeHtml('Rock & Roll'), 'Rock & Roll');
assert.equal(
escapeHtml('<blank>'), '<blank>');
literals/templating_test.mjs
[ES6]
24 Symbols
Symbols are primitive values that are created via the factory function
Symbol():
for debugging.
const obj = {
[sym]: 123,
};
Even though symbols are primitives, they are also like objects in that each
Prior to symbols, objects were the best choice if we needed values that were
assert.equal(mySymbol.toString(),
'Symbol(mySymbol)');
Second, since ES2019, we can retrieve the description via the property
.description:
assert.equal(mySymbol.description, 'mySymbol');
Let’s assume you want to create constants representing the colors red,
orange, yellow, green, blue, and violet. One simple way of doing so would
be to use strings:
On the plus side, logging that constant produces helpful output. On the
because two strings with the same content are considered equal:
assert.notEqual(COLOR_BLUE, MOOD_BLUE);
function getComplement(color) {
switch (color) {
case COLOR_RED:
return COLOR_GREEN;
case COLOR_ORANGE:
return COLOR_BLUE;
case COLOR_YELLOW:
return COLOR_VIOLET;
case COLOR_GREEN:
return COLOR_RED;
case COLOR_BLUE:
return COLOR_ORANGE;
case COLOR_VIOLET:
return COLOR_YELLOW;
default:
throw new Exception('Unknown color: '+color);
}
}
assert.equal(getComplement(COLOR_YELLOW),
COLOR_VIOLET);
example:
data and provide services that are not part of the problem domain – for
example:
const point = {
x: 7,
y: 4,
toString() {
return `(${this.x}, ${this.y})`;
},
};
assert.equal(
String(point), '(7, 4)'); // (A)
domain.
const point = {
x: 7,
y: 4,
toJSON() {
return [this.x, this.y];
},
};
assert.equal(
JSON.stringify(point), '[7,4]');
property.
The base level and the meta-level of a program must be independent: Base-
level property keys should not be in conflict with meta-level property keys.
language and the meta-level names of libraries would still exist in the
These are two examples of where the latter was an issue for JavaScript:
.at() because the former name was already used by library (source).
Symbols, used as property keys, help us here: Each symbol is unique and a
symbol key never clashes with any other string or symbol key.
property key for such a method and implementing it for an object would
look like:
have the key specialMethod. More details are explained in “Computed keys in
Symbols that play special roles within ECMAScript are called publicly
const PrimitiveNull = {
[Symbol.hasInstance](x) {
return x === null;
}
};
assert.equal(null instanceof PrimitiveNull,
true);
> String({})
'[object Object]'
> String({ [Symbol.toStringTag]: 'is no money' })
'[object is no money]'
Symbol.toStringTag: exercises/symbols/to_string_tag_test.mjs
Symbol.hasInstance: exercises/symbols/has_instance_test.mjs
One key pitfall with symbols is how often exceptions are thrown when
converting them to something else. What is the thinking behind that? First,
conversion to number never makes sense and should be warned about.
output. But it also makes sense to warn about accidentally turning a symbol
The downside is that the exceptions make working with symbols more
25.1.1 break
25.1.3 continue
[ES1]
25.3 if statements
[ES3]
25.4 switch statements
[ES1]
25.5 while loops
[ES3]
25.6 do-while loops
[ES1]
25.7 for loops
[ES6]
25.8 for-of loops
[ES2018]
25.9 for-await-of loops
[ES1]
25.10 for-in loops (avoid)
25.11 Recomendations for looping
[ES1]
if statement
[ES3]
switch statement
[ES1]
while loop
[ES3]
do-while loop
[ES1]
for loop
[ES6]
for-of loop
[ES2018]
for-await-of loop
[ES1]
for-in loop
The two operators break and continue can be used to control loops and other
25.1.1 break
There are two versions of break: one with an operand and one without an
operand. The latter version works inside the following statements: while, do-
while, for, for-of, for-await-of, for-in and switch. It immediately leaves the
current statement:
Output:
a
---
b
break with an operand works everywhere. Its operand is a label. Labels can
be put in front of any statement, including blocks. break my_label leaves the
my_label: { // label
if (condition) break my_label; // labeled break
// ···
}
Fail: The loop finishes without finding a result. That is handled directly
Succeed: While looping, we find a result. Then we use break plus label
25.1.3 continue
continue only works inside while, do-while, for, for-of, for-await-of, and for-in.
It immediately leaves the current loop iteration and continues with the next
one – for example:
const lines = [
'Normal line',
'# Comment',
'Another normal line',
];
for (const line of lines) {
if (line.startsWith('#')) continue;
console.log(line);
}
Output:
Normal line
Another normal line
if, while, and do-while have conditions that are, in principle, boolean.
if (value) {}
if (Boolean(value) === true) {}
undefined, null
false
0, NaN
0n
''
All other values are truthy. For more information, see “Falsy and truthy
values” (§17.2).
These are two simple if statements: one with just a “then” branch and one
if (cond) {
// then branch
}
if (cond) {
// then branch
} else {
// else branch
}
if (cond1) {
// ···
} else if (cond2) {
// ···
}
if (cond1) {
// ···
} else if (cond2) {
// ···
} else {
// ···
}
if («cond») «then_statement»
else «else_statement»
So far, the then_statement has always been a block, but we can use any
That means that else if is not its own construct; it’s simply an if statement
switch («switch_expression») {
«switch_body»
}
case «case_expression»:
«statements»
default:
«statements»
It jumps to the first case clause whose expression has the same result as
function dayOfTheWeek(num) {
switch (num) {
case 1:
return 'Monday';
case 2:
return 'Tuesday';
case 3:
return 'Wednesday';
case 4:
return 'Thursday';
case 5:
return 'Friday';
case 6:
return 'Saturday';
case 7:
return 'Sunday';
}
}
assert.equal(dayOfTheWeek(5), 'Friday');
At the end of a case clause, execution continues with the next case clause,
function englishToFrench(english) {
let french;
switch (english) {
case 'hello':
french = 'bonjour';
case 'goodbye':
french = 'au revoir';
}
return french;
}
// The result should be 'bonjour'!
assert.equal(englishToFrench('hello'), 'au revoir');
function englishToFrench(english) {
let french;
switch (english) {
case 'hello':
french = 'bonjour';
break;
case 'goodbye':
french = 'au revoir';
break;
}
return french;
}
assert.equal(englishToFrench('hello'), 'bonjour');
// ok
function isWeekDay(name) {
switch (name) {
case 'Monday':
case 'Tuesday':
case 'Wednesday':
case 'Thursday':
case 'Friday':
return true;
case 'Saturday':
case 'Sunday':
return false;
}
}
assert.equal(isWeekDay('Wednesday'), true);
assert.equal(isWeekDay('Sunday'), false);
function isWeekDay(name) {
switch (name) {
case 'Monday':
case 'Tuesday':
case 'Wednesday':
case 'Thursday':
case 'Friday':
return true;
case 'Saturday':
case 'Sunday':
return false;
default:
throw new Error('Illegal value: '+name);
}
}
assert.throws(
() => isWeekDay('January'),
{message: 'Illegal value: January'});
Exercises: switch
exercises/control-flow/number_to_month_test.mjs
Bonus: exercises/control-flow/is_object_via_switch_test.mjs
while («condition») {
«statements»
}
Before each loop iteration, while evaluates condition:
If the result is truthy, the while body is executed one more time.
The following code uses a while loop. In each loop iteration, it removes the
Output:
a
b
c
while (true) {
if (Math.random() === 0) break;
}
let input;
do {
input = prompt('Enter text:');
console.log(input);
} while (input !== ':q');
do-while can also be viewed as a while loop that runs at least once.
The first line is the head of the loop and controls how often the body (the
remainder of the loop) is executed. It has three parts and each of them is
optional:
«initialization»
while («condition») {
«statements»
«post_iteration»
}
As an example, this is how to count from zero to two via a for loop:
Output:
0
1
2
Output:
a
b
c
for (;;) {
if (Math.random() === 0) break;
}
A for-of loop iterates over any iterable – a data container that supports the
the head:
Output:
hello
world
Note that in for-of loops we can use const. The iteration variable can still be
different for each iteration (it just can’t change during the iteration). Think
In contrast, in for loops we must declare variables via let or var if their
values change.
As mentioned before, for-of works with any iterable object, not just with
Lastly, we can also use for-of to iterate over the [index, element] entries of
Arrays:
Output:
0 -> a
1 -> b
2 -> c
Exercise: for-of
exercises/control-flow/array_to_string_test.mjs
synchronous ones. And it can only be used inside async functions and async
generators.
The for-in loop visits all (own and inherited) enumerable property keys of
It visits all enumerable property keys (both own and inherited ones),
0
1
2
propKey
For looping over a synchronous iterable (in ES6+), you must use for-of.
For looping over an Array in ES5+, you can use the Array method
.forEach().
Before ES5, you can use a plain for loop to loop over an Array.
26.2 throw
[ES2022]
26.5.2 Chaining errors via error.cause
JavaScript didn’t support exceptions until ES3. That explains why they
Consider the following code. It reads profiles stored in files into an Array
function readProfiles(filePaths) {
const profiles = [];
for (const filePath of filePaths) {
try {
const profile = readOneProfile(filePath);
profiles.push(profile);
} catch (err) { // (A)
console.log('Error in: '+filePath, err);
}
}
}
function readOneProfile(filePath) {
const profile = new Profile();
const file = openFile(filePath);
// ··· (Read the data in `file` into `profile`)
return profile;
}
function openFile(filePath) {
if (!fs.existsSync(filePath)) {
throw new Error('Could not find file
'+filePath); // (B)
}
// ··· (Open the file whose path is `filePath`)
}
Let’s examine what happens in line B: An error occurred, but the best place
to handle the problem is not the current location, it’s line A. There, we can
Therefore:
readProfiles(···)
for (const filePath of filePaths)
try
readOneProfile(···)
openFile(···)
if (!fs.existsSync(filePath))
throw
One by one, throw exits the nested constructs, until it encounters a try
26.2 throw
throw «value»;
26.2.1 What values should we throw?
Any value can be thrown in JavaScript. However, it’s best to use instances of
traces and error chaining (see “Error and its subclasses” (§26.4)).
instances:
try {
«try_statements»
} catch (error) {
«catch_statements»
} finally {
«finally_statements»
}
try-catch
try-finally
try-catch-finally
The try block can be considered the body of the statement. This is where we
the catch clause and the code in that clause is executed. Next, execution
normally continues after the try statement. That may change if:
statement ends).
The following code demonstrates that the value that is thrown in line A is
try {
func();
} catch (err) { // (B)
assert.equal(err, errorObject);
}
We can omit the catch parameter if we are not interested in the value that
was thrown:
try {
// ···
} catch {
// ···
}
That may occasionally be useful. For example, Node.js has the API function
catch parameter and would, for example, check that its type is as expected.
The code inside the finally clause is always executed at the end of a try
statement – no matter what happens in the try block or the catch clause.
Let’s look at a common use case for finally: We have created a resource and
want to always destroy it when we are done with it, no matter what happens
The finally clause is always executed, even if an error is thrown (line A):
class Error {
// Instance properties
message: string;
cause?: any; // ES2022
stack: string; // non-standard but widely
supported
constructor(
message: string = '',
options?: ErrorOptions // ES2022
);
}
interface ErrorOptions {
cause?: any; // ES2022
}
The subsections after the next one explain the instance properties .message,
26.4.1.1 Error.prototype.name
> Error.prototype.name
'Error'
> RangeError.prototype.name
'RangeError'
Therefore, there are two ways to get the name of the class of a built-in error
object:
The instance property .cause is created via the options object in the second
parameter of new Error(). It specifies which other error caused the current
one.
const err = new Error('msg', {cause: 'the cause'});
assert.equal(err.cause, 'the cause');
For information on how to use this property see “Chaining errors” (§26.5).
[ES2021]
AggregateError represents multiple errors at once. In the standard
values.
detected.
failure cause.
URIError indicates that one of the global URI handling functions was
Since ECMAScript 2022, the Error constructor accepts two parameters (see
We can either omit the constructor in our subclass or we can invoke super()
like this:
class MyCustomError extends Error {
constructor(message, options) {
super(message, options);
// ···
}
}
Sometimes, we catch errors that are thrown during a more deeply nested
function readFiles(filePaths) {
return filePaths.map(
(filePath) => {
try {
const text = readText(filePath);
const json = JSON.parse(text);
return processJson(json);
} catch (error) {
// (A)
}
});
}
The statements inside the try clause may throw all kinds of errors. In most
cases, an error won’t be aware of the path of the file that caused it. That’s
[ES2022]
26.5.2 Chaining errors via error.cause
Since ECMAScript 2022, new Error() lets us specify what caused it:
function readFiles(filePaths) {
return filePaths.map(
(filePath) => {
try {
// ···
} catch (error) {
throw new Error(
`While processing ${filePath}`,
{cause: error}
);
}
});
}
/**
* An error class that supports error chaining.
* If there is built-in support for .cause, it uses
it.
* Otherwise, it creates this property itself.
*/
class CausedError extends Error {
constructor(message, options) {
super(message, options);
if (
(isObject(options) && 'cause' in options)
&& !('cause' in this)
) {
// .cause was specified but the
superconstructor
// did not create an instance property.
const cause = options.cause;
this.cause = cause;
if ('stack' in cause) {
this.stack = this.stack + '\nCAUSE: ' +
cause.stack;
}
}
}
}
function isObject(value) {
return value !== null && typeof value ===
'object';
}
exercises/exception-handling/call_function_test.mjs
27 Callable values
expressions
[ES6]
27.3 Specialized functions
ordinary functions
[ES6]
27.6.4 Parameter default values
[ES6]
27.6.5 Rest parameters
[ES6]
27.6.8 Spreading (...) into function calls
Real function
Method
Constructor function
A specialized function can only play one of those roles – for example:
The following code shows two ways of doing (roughly) the same thing:
scope and activation” (§13.8)) and can be called before they are declared.
Variable declarations, such as the one for ordinary2, are not activated early.
have names:
myName is only accessible inside the body of the function. The function can
Even if they are not assigned to variables, named function expressions have
function getNameOfCallback(callback) {
return callback.name;
}
assert.equal(
getNameOfCallback(function () {}), ''); //
anonymous
assert.equal(
getNameOfCallback(function named() {}), 'named');
// (A)
function funcDecl() {}
assert.equal(
getNameOfCallback(funcDecl), 'funcDecl');
One benefit of functions having names is that those names show up in error
stack traces.
A function expression
Let’s examine the parts of a function declaration via the following example.
function add(x, y) {
return x + y;
}
The curly braces ({ and }) and everything between them are the body of
[ES2017]
27.2.3.1 Trailing commas in parameter lists
literals. Since ES5, they are also allowed in object literals. Since ES2017,
we can add trailing commas to parameter lists (declarations and
invocations):
// Declaration
function retrieveData(
contentText,
keyword,
{unique, ignoreCase, pageSize}, // trailing comma
) {
// ···
}
// Invocation
retrieveData(
'',
null,
{ignoreCase: true, pageSize: 10}, // trailing
comma
);
function add(x, y) {
return x + y;
}
This function declaration creates an ordinary function whose name is add.
The distinction between the concepts syntax, entity, and role is subtle and
often doesn’t matter. But I’d like to sharpen your eye for it:
function is an entity.
and classes.
Syntax is the code that we use to create entities. Function declarations
called arrow functions. The same is true for methods and classes.
A role describes how we use entities. The entity ordinary function can
play the role real function, or the role method, or the role class. The
entity arrow function can also play the role real function.
function.
Many other programming languages only have a single entity that plays the
role real function. Then they can use the name function for both role and
entity.
[ES6]
27.3 Specialized functions
const obj = {
myMethod() {
return 'abc';
}
};
assert.equal(obj.myMethod(), 'abc');
class MyClass {
/* ··· */
}
const inst = new MyClass();
Apart from nicer syntax, each kind of specialized function also supports
new features, making them better at their jobs than ordinary functions.
Method Constructor
Function call
call call
function undefined)
Arrow ✔ (lexical ✘
function this)
Class ✘ ✘ ✔
It’s important to note that arrow functions, methods, and classes are still
categorized as functions:
2. They work better as real functions inside methods: Methods can refer
to the object that received a method call via the special variable this.
We’ll first examine the syntax of arrow functions and then how this works
in various functions.
are expressions.
Here, the body of the arrow function is a block. But it can also be an
expression. The following arrow function works exactly like the previous
one.
identifier (not a destructuring pattern) then you can omit the parentheses
around the parameter:
const id = x => x;
functions or methods:
If you don’t, JavaScript thinks, the arrow function has a block body (that
This pitfall is caused by syntactic ambiguity: object literals and code blocks
have the same syntax. We use the parentheses to tell JavaScript that the
arrow functions
We are taking a quick look at the special variable this here, in order to
understand why arrow functions are better real functions than ordinary
functions.
Inside methods, the special variable this lets us access the receiver – the
const obj = {
myMethod() {
assert.equal(this, obj);
}
};
obj.myMethod();
Ordinary functions can be methods and therefore also have the implicit
parameter this:
const obj = {
myMethod: function () {
assert.equal(this, obj);
}
};
obj.myMethod();
real function. Then its value is undefined (if strict mode is active, which it
function ordinaryFunc() {
assert.equal(this, undefined);
}
ordinaryFunc();
That means that an ordinary function, used as a real function, can’t access
the this of a surrounding method (line A). In contrast, arrow functions don’t
have this as an implicit parameter. They treat it like any other variable and
const jill = {
name: 'Jill',
someMethod() {
function ordinaryFunc() {
assert.throws(
() => this.name, // (A)
/^TypeError: Cannot read properties of
undefined \(reading 'name'\)$/);
}
ordinaryFunc();
which is undefined (as filled in by the function call). Given that ordinary
This time, we succeed because the arrow function does not have its
own this. this is resolved lexically, just like any other variable. That’s
functions
When it comes to real functions, the choice between an arrow function and
winners, due to their compact syntax and them not having this as an
implicit parameter:
rule.
function timesOrdinary(x, y) {
return x * y;
}
const timesArrow = (x, y) => {
return x * y;
};
This section mainly serves as a reference for the current and upcoming
So far, all (real) functions and methods, that we have seen, were:
Single-result
Synchronous
combinations:
methods.
Table 27.2 gives an overview of the syntax for creating these 4 kinds of
Result #
f = function () {}
f = () => {}
function method
Result #
f = function* () {}
f = async function () {}
f = async () => {}
function method
iterable
f = async function* () {}
Table 27.2: Syntax for creating functions and methods. The last
methods.)
Another example:
function boolToYesNo(bool) {
if (bool) {
return 'Yes';
} else {
return 'No';
}
}
assert.equal(boolToYesNo(true), 'Yes');
assert.equal(boolToYesNo(false), 'No');
function noReturn() {
// No explicit return
}
assert.equal(noReturn(), undefined);
The term parameter and the term argument basically mean the same thing.
Arguments are part of a function call. They are also called actual
Output:
a
b
For example:
function foo(x, y) {
return [x, y];
}
[ES6]
27.6.4 Parameter default values
Parameter default values specify the value to use if a parameter has not been
assert.deepEqual(
f(undefined, undefined),
[undefined, 0]);
[ES6]
27.6.5 Rest parameters
for example:
There are two restrictions related to how we can use rest parameters:
We cannot use more than one rest parameter per function definition.
assert.throws(
() => eval('function f(...x, ...y) {}'),
/^SyntaxError: Rest parameter must be last
formal parameter$/
);
assert.throws(
() => eval('function f(...restParams,
lastParam) {}'),
/^SyntaxError: Rest parameter must be last
formal parameter$/
);
function createPoint(x, y) {
return {x, y};
// same as {x: x, y: y}
}
When someone calls a function, the arguments provided by the caller are
have the same position. A function call with only positional arguments
looks as follows.
selectEntries(3, 20, 2)
have the same name. JavaScript doesn’t have named parameters, but
you can simulate them. For example, this is a function call with only
The order of the arguments doesn’t matter (as long as the names are
correct).
can easily provide any subset of all optional parameters and don’t have
to be aware of the ones they omit (with positional parameters, you have
But it does not work if you call the function without any parameters:
> selectEntries()
TypeError: Cannot read properties of undefined
(reading 'start')
You can fix this by providing a default value for the whole pattern. This
default value works the same as default values for simpler parameter
[ES6]
27.6.8 Spreading (...) into function calls
If you put three dots (...) in front of the argument of a function call, then
you spread it. That means that the argument must be an iterable object and
the iterated values all become arguments. In other words, a single argument
Output:
a
b
Spreading and rest parameters use the same syntax (...), but they serve
opposite purposes:
Math.max() returns the largest one of its zero or more arguments. Alas, it can’t
Similarly, the Array method .push() destructively adds its zero or more
by spreading:
arr1.push(...arr2);
assert.deepEqual(arr1, ['a', 'b', 'c', 'd']);
Positional parameters:
exercises/callables/positional_parameters_test.mjs
Functions are objects and have methods. In this section, we look at three of
However, with .call(), we can also specify a value for the implicit parameter
this. In other words: .call() makes the implicit parameter this explicit.
function func(x, y) {
return [this, x, y];
}
assert.deepEqual(
func.call('hello', 'a', 'b'),
['hello', 'a', 'b']);
undefined:
assert.deepEqual(
func('a', 'b'),
[undefined, 'a', 'b']);
In arrow functions, the value for this provided via .call() (or other means) is
ignored.
However, with .apply(), we can also specify a value for the implicit
parameter this.
function func(x, y) {
return [this, x, y];
}
follows:
someFunc() with this set to thisValue and these parameters: arg1, arg2, followed
boundFunc('a', 'b')
someFunc.call(thisValue, arg1, arg2, 'a', 'b')
function as follows:
function bind(func, thisValue, ...boundArgs) {
return (...args) =>
func.call(thisValue, ...boundArgs, ...args);
}
provide a value for this. Given that it is undefined during function calls, it is
function add(x, y) {
return x + y;
}
28.1 eval()
28.3 Recommendations
28.1 eval()
Given a string str with JavaScript code, eval(str) evaluates that code and
Directly, via a function call. Then the code in its argument is evaluated
Indirectly, not via a function call. Then it evaluates its code in global
scope.
“Not via a function call” means “anything that looks different than
eval(···)”:
eval.call(undefined, '···') (uses method .call() of functions)
globalThis.eval('···')
Etc.
globalThis.myVariable = 'global';
function func() {
const myVariable = 'local';
// Direct eval
assert.equal(eval('myVariable'), 'local');
// Indirect eval
assert.equal(eval.call(undefined, 'myVariable'),
'global');
}
Evaluating code in global context is safer because the code has access to
fewer internals.
In the next example, we create the same function twice, first via new
manually.
28.3 Recommendations
Security Policy.
Very often, JavaScript is dynamic enough so that you don’t need eval() or
similar. In the following example, what we are doing with eval() (line A)
Prefer new Function() over eval(): it always executes its code in global
Prefer indirect eval over direct eval: evaluating code in global context is
safer.
VI Modularity
[ES6]
29 Modules
ECMAScript 5
modules
[ES2020]
29.13 import.meta – metadata for the current module
29.13.1 import.meta.url
[ES2020]
29.14 Loading modules dynamically via import()
(advanced)
(advanced)
named export of that module. All other entities are private to the module.
// Namespace import
import * as lib1 from './lib1.mjs';
assert.equal(lib1.one, 1);
assert.equal(lib1.myFunc(), 3);
The string after from is called a module specifier. It identifies from which
So far, all imports we have seen were static, with the following constraints:
[ES2020]
Dynamic imports via import() don’t have those constraints:
A default export is mainly used when a module only contains a single entity
always have names: In the previous example, we can omit the name getHello.
There can be at most one default export. That’s why const or let can’t be
'https://www.unpkg.com/browse/yargs@17.3.1/browse
r.mjs'
'file:///opt/nodejs/config.mjs'
Absolute specifiers are mostly used to access libraries that are directly
for example:
'./sibling-module.js'
'../module-in-parent-dir.mjs'
'../../dir/other-module.js'
Relative specifiers are mostly used to access other modules within the
Bare specifiers are paths (without protocol and domain) that start with
neither slashes nor dots. They begin with the names of packages (as
'some-package'
'some-package/sync'
'some-package/util/files/path-tools.js'
'@some-scope/scoped-name'
'@some-scope/scoped-name/async'
'@some-scope/scoped-name/dir/some-module.mjs'
package.
built-in modules, but the source code formats that came before them, are
still around, too. Understanding the latter helps understand the former, so
let’s investigate. The next sections describe the following ways of delivering
Scripts are code fragments that browsers run in global scope. They are
precursors of modules.
Table 29.1 gives an overview of these code formats. Note that for
Filename
Runs on Loaded
ext.
module
module servers
Before we get to built-in modules (which were introduced with ES6), all
code that we’ll see, will be written in ES5. Among other things:
Initially, browsers only had scripts – pieces of code that were executed in
global scope. As an example, consider an HTML file that loads script files
<script src="other-module1.js"></script>
<script src="other-module2.js"></script>
<script src="my-module.js"></script>
// Body
function internalFunc() {
// ···
}
function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
IIFE? var is not block-scoped (like const and let), it is function-scoped: the
only way to create new scopes for var-declared variables is via functions or
methods (with const and let, we can use either functions, methods, or blocks
{}). Therefore, the IIFE in the example hides all of the following variables
internalFunc, exportedFunc.
Note that we are using an IIFE in a particular manner: at the end, we pick
what we want to export and return it via an object literal. That is called the
Dependencies are not stated explicitly, and there is no built-in way for
a script to load the scripts it depends on. Therefore, the web page has
to load not just the scripts that are needed by the page but also the
custom module systems within the language. Two popular ones are:
popularity were the npm package manager for Node and tools that enabled
using Node modules on the client side (browserify, webpack, and others).
From now on, CommonJS module means the Node.js version of this
CommonJS module:
// Imports
var importedFunc1 = require('./other-
module1.js').importedFunc1;
var importedFunc2 = require('./other-
module2.js').importedFunc2;
// Body
function internalFunc() {
// ···
}
function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
// Exports
module.exports = {
exportedFunc: exportedFunc,
};
Compact syntax.
The AMD module format was created to be easier to use in browsers than
define(['./other-module1.js', './other-module2.js'],
function (otherModule1, otherModule2) {
var importedFunc1 = otherModule1.importedFunc1;
var importedFunc2 = otherModule2.importedFunc2;
function internalFunc() {
// ···
}
function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
return {
exportedFunc: exportedFunc,
};
});
requirement for browsers, where code can’t wait until a module has
available.
systems emerge:
IDs.
They continue the tradition of JavaScript modules and have all of their
loading.
function internalFunc() {
···
}
1. Syntax (how code is written): What is a module? How are imports and
lib/my-math.mjs
main.mjs
Entities that are not exported are private to a module and can’t be accessed
from outside.
assert.deepEqual(
Object.keys(myMath), ['LIGHTSPEED', 'square']);
The named export style we have seen so far was inline: We exported entities
But we can also use separate export clauses. For example, this is what
function times(a, b) {
return a * b;
}
function square(x) {
return times(x, x);
}
const LIGHTSPEED = 299792458;
With an export clause, we can rename before exporting and use different
names internally:
function times(a, b) {
return a * b;
}
function sq(x) {
return times(x, x);
}
const LS = 299792458;
export {
sq as square,
LS as LIGHTSPEED, // trailing comma is optional
};
Each module can have at most one default export. The idea is that the
my-func.mjs
main.mjs
Note the syntactic difference: the curly braces around named imports
indicate that we are reaching into the module, while a default import is the
module.
The most common use case for a default export is a module that
The reason is that export default can’t be used to label const: const may define
multiple values, but export default needs exactly one value. Consider the
With this code, we don’t know which one of the three values is the default
export.
Exercise: Default exports
exercises/modules/export_default_test.mjs
export:
export {
greet as default,
};
imports:
const obj = {
default: 123,
};
assert.equal(obj.default, 123);
29.8 Re-exporting
assert.deepEqual(
Object.keys(library),
['DEF', 'INTERNAL_DEF', 'func', 'internalFunc',
'ns']
);
assert.deepEqual(
Object.keys(library.ns),
['INTERNAL_DEF', 'default', 'internalFunc']
);
So far, we have used imports and exports intuitively, and everything seems
to have worked as expected. But now it is time to take a closer look at how
counter.mjs
main.mjs
that the connection to counter is live – we can always access the live state of
that variable:
import { counter, incCounter } from './counter.mjs';
Note that while the connection is live and we can read counter, we cannot
become exports.
this case.
Figure 29.1: A directed graph of modules importing modules: M
its exports. Before a parent can be instantiated, all of its children must
be instantiated.
modules:
P can already mention imports from M. They just can’t use them, yet,
because the imported values are filled in later. For example, a function
in P can access an import from M. The only limitation is that we must
views” on exports.
libraries and apps for Node.js and web browsers. It is managed via the npm
package.json at the top level that describes the package. For example, when
package.json:
{
"name": "my-package",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" &&
exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Some of these properties contain simple metadata:
name specifies the name of this package. Once it is uploaded to the npm
are made.
are made.
main: specifies the module that “is” the package (explained later in this
chapter).
scripts: are commands that we can execute via npm run. For example, the
usually many of these directories. Which one npm uses, depends on the
directory where one currently is. For example, if we are inside a directory
/tmp/a/b/, npm tries to find a node_modules in the current directory, its parent
directory, the parent directory of the parent, etc. In other words, it searches
/tmp/a/b/node_modules
/tmp/a/node_modules
/tmp/node_modules
When installing a package some-pkg, npm uses the closest node_modules. If, for
/tmp/a/b/node_modules/some-pkg/
that works, is explained later. For now, consider the following example:
// /home/jane/proj/main.mjs
import * as theModule from 'the-package/the-
module.mjs';
modules), Node.js walks up the node_module chain and searches the following
locations:
/home/jane/proj/node_modules/the-package/the-module.mjs
/home/jane/node_modules/the-package/the-module.mjs
/home/node_modules/the-package/the-module.mjs
Node.js. So why can we also use npm to install libraries for browsers?
That is enabled via bundling tools, such as webpack, that compile and
There are no established best practices for naming module files and the
The names of module files are dash-cased and start with lowercase
letters:
./my-module.mjs
./some-func.mjs
we avoid camel case, so that “local” files have names that are
consistent with those of npm packages. Using only lowercase letters
and file systems that aren’t: the former distinguish files whose names
have the same letters, but with different cases; the latter don’t.
There are clear rules for translating dash-cased file names to camel-
imports, these rules work for both namespace imports and default
imports.
I also like underscore-cased module file names because we can directly use
But that style does not work for default imports: I like underscore-casing for
Module specifiers are the strings that identify modules. They work slightly
'/home/jane/file-tools.mjs'
'https://example.com/some-module.mjs'
'file:///home/john/tmp/main.mjs'
Bare path: does not start with a dot, a slash or a protocol, and consists
'lodash'
'the-package'
Deep import path: starts with a bare path and has at least one slash.
Example:
'the-package/dist/the-module.mjs'
Relative paths, absolute paths, and URLs work as expected. They all
must point to real files (in contrast to CommonJS, which lets us omit
How bare paths will end up being handled is not yet clear. We will
tables.
Note that bundling tools such as webpack, which combine modules into
fewer files, are often less strict with specifiers than browsers. That’s because
they operate at build/compile time (not at runtime) and can search for files
Relative paths are resolved as they are in web browsers – relative to the
use URLs that start with file:///. We can create such URLs via
url.pathToFileURL().
(similarly to CommonJS).
Deep import paths are also resolved relatively to the closest node_modules
All specifiers, except bare paths, must refer to actual files. That is, ESM
a "main" property.
All built-in Node.js modules are available via bare paths and have named
assert.equal(
path.join('a/b/c', '../d'), 'a/b/d');
The filename extension .js stands for either ESM or CommonJS. Which one
it is is configured via the “closest” package.json (in the current directory, the
parent directory, etc.). Using package.json in this manner is independent of
packages.
Not all source code executed by Node.js comes from files. We can also send
it code via stdin, --eval, and --print. The command line option --input-type
As ESM: --input-type=module
29.13.1 import.meta.url
'https://example.com/code/main.mjs'
29.13.2 import.meta.url and class URL
In other words, this constructor lets us resolve a relative path against a base
URL:
This is how we get a URL instance that points to a file data.txt that sits next to
example:
'file:///Users/rauschma/my-module.mjs'
Many Node.js file system operations accept either strings with paths or
instances of URL. That enables us to read a sibling file data.txt of the current
module:
For most functions of the module fs, we can refer to files via:
For more information on this topic, see the Node.js API documentation.
If we need a path that can be used in the local file system, then property
assert.equal(
new URL('file:///tmp/with%20space.txt').pathname,
'/tmp/with%20space.txt');
absolute path.
So far, the only way to import a module has been via an import statement.
We must use it at the top level of a module. That is, we can’t, for
if statement.
The module specifier is always fixed. That is, we can’t change what we
dynamically.
import(moduleSpecifierStr)
.then((namespaceObject) => {
console.log(namespaceObject.namedExport);
});
Note that await can be used at the top levels of modules (see next section).
lib/my-math.mjs
main1.mjs
main2.mjs
// main1.mjs
const moduleSpecifier = './lib/my-math.mjs';
function mathOnDemand() {
return import(moduleSpecifier)
.then(myMath => {
const result = myMath.LIGHTSPEED;
assert.equal(result, 299792458);
return result;
});
}
await mathOnDemand()
.then((result) => {
assert.equal(result, 299792458);
});
Next, we’ll implement the same functionality as in main1.mjs but via a feature
Promises.
// main2.mjs
const moduleSpecifier = './lib/my-math.mjs';
function:
module.
it can be loaded on demand. Then import() helps because we can put such
For example, a module with a polyfill that makes a new feature available on
legacy platforms:
if (isLegacyPlatform()) {
import('./my-polyfill.mjs')
.then(···);
}
We can use the await operator at the top level of a module. If we do that, the
language.
Why would we want to use the await operator at the top level of a module? It
lets us initialize a module with asynchronously loaded data. The next three
let mylib;
try {
mylib = await
import('https://primary.example.com/mylib');
} catch {
mylib = await
import('https://secondary.example.com/mylib');
}
finishes first.
29.15.2 How does top-level await work under the hood?
first.mjs:
main.mjs:
first.mjs:
main.mjs:
import {promise as firstPromise, first} from
'./first.mjs';
import {promise as secondPromise, second} from
'./second.mjs';
export const promise = (async () => { // (B)
await Promise.all([firstPromise, secondPromise]);
// (C)
assert.equal(first, 'First!');
assert.equal(second, 'Second!');
})();
fulfilled after its body was executed. At that point, it is safe to access the
In case (2), the importing module waits until the Promises of all imported
asynchronous modules are fulfilled, before it enters its body (line C).
functions.
modules. Therefore, it’s best used sparingly. Asynchronous tasks that take
However, even modules without top-level await can block importers (e.g. via
against it.
(advanced)
Polyfills help with a conflict that we are facing when developing a web
application in JavaScript:
On one hand, we want to use modern web platform features that make
possible.
Given a web platform feature X:
the feature available on the platform. In the latter case, the polyfilled
order to achieve that, the polyfill usually makes global changes. For
implementation of X.
ponyfill
There is also the term shim, but it doesn’t have a universally agreed
Every time our web applications starts, it must first execute all polyfills for
Inspiration for the term replica: The Eiffel Tower in Las Vegas
[ES2018]
30.4 Spreading into object literals (...)
properties
properties
[ES6]
30.4.4 “Destructive spreading”: Object.assign()
[ES2020]
(advanced)
30.7.5 Enumerability
[ES2019]
30.7.10 Assembling objects via Object.fromEntries()
[ES5]
30.8 Property attributes and property descriptors
(advanced)
[ES5]
30.9 Protecting objects from being changed (advanced)
mutated
[ES2022]
properties?
30.12.6 Object.prototype.*
introduced in four steps. This chapter covers step 1 and 2; the next chapter
mechanism.
3. Classes (next chapter): JavaScript’s classes are factories for objects.
SuperClass
superData
superMthd
mthd ƒ
MyClass SubClass
mthd ƒ __proto__ data subData
data 4 data 4 mthd subMthd
Creating an object via an object literal (starts and ends with a curly brace):
assert.equal(
myObject.myProperty, 1
);
assert.equal(
myObject.myMethod(), 2
);
assert.equal(
myObject.myAccessor, 1
);
myObject.myAccessor = 3;
assert.equal(
myObject.myProperty, 3
);
Being able to create objects directly (without classes) is one of the highlights
of JavaScript.
const original = {
a: 1,
b: {
c: 3,
},
};
assert.deepEqual(
modifiedCopy,
{
a: 1,
b: {
c: 3,
},
d: 4,
}
);
object:
classes are based on it. Each object has null or an object as its prototype. The
latter object can also have a prototype, etc. In general, we get chains of
prototypes.
Notes:
The most important use case for prototypes is that several objects can share
Objects in JavaScript:
Private slots can only be created via classes and are explained in
const fixedLayoutObject = {
product: 'carrot',
quantity: 4,
};
Dictionary objects: Used this way, objects work like lookup tables or
maps. They have a variable number of properties, whose keys are not
known at development time. All of their values have the same type.
const dictionaryObject = {
['one']: 1,
['two']: 2,
};
Note that the two ways can also be mixed: Some objects are both fixed-
The ways of using objects influence how they are explained in this chapter:
First, we’ll explore fixed-layout objects. Even though property keys are
strings or symbols under the hood, they will appear as fixed identifiers
to us.
Later, we’ll explore dictionary objects. Note that Maps are usually
Object literals are one way of creating fixed-layout objects. They are a stand-
const jane = {
first: 'Jane',
last: 'Doe', // optional trailing comma
};
In the example, we created an object via an object literal, which starts and
ends with curly braces {}. Inside it, we defined two properties (key-value
entries):
The first property has the key first and the value 'Jane'.
The second property has the key last and the value 'Doe'.
We will later see other ways of specifying property keys, but with this way of
specifying them, they must follow the rules of JavaScript variable names.
For example, we can use first_name as a property key, but not first-name).
const obj = {
if: true,
const: true,
};
occasionally use Object.keys() in this part of the chapter. It lists property keys:
function createPoint(x, y) {
return {x, y}; // Same as: {x: x, y: y}
}
assert.deepEqual(
createPoint(9, 2),
{ x: 9, y: 2 }
);
const jane = {
first: 'Jane',
last: 'Doe',
};
assert.equal(jane.unknownProperty, undefined);
obj.unknownProperty = 'abc';
assert.deepEqual(
Object.keys(obj), ['unknownProperty']);
The following code shows how to create the method .says() via an object
literal:
const jane = {
first: 'Jane', // value property
says(text) { // method
return `${this.first} says “${text}”`; // (A)
}, // comma as separator (optional at end)
};
assert.equal(jane.says('hello'), 'Jane says
“hello”');
During the method call jane.says('hello'), jane is called the receiver of the
method call and assigned to the special variable this (more on this in
“Methods and the special variable this” (§30.5)). That enables method
An accessor is defined via syntax inside an object literal that looks like
methods: a getter and/or a setter (i.e., each accessor has one or both of
them).
30.3.6.1 Getters
const jane = {
first: 'Jane',
last: 'Doe',
get full() {
return `${this.first} ${this.last}`;
},
};
assert.equal(jane.full, 'Jane Doe');
jane.first = 'John';
assert.equal(jane.full, 'John Doe');
30.3.6.2 Setters
const jane = {
first: 'Jane',
last: 'Doe',
set full(fullName) {
const parts = fullName.split(' ');
this.first = parts[0];
this.last = parts[1];
},
};
exercises/objects/color_point_object_test.mjs
30.4 Spreading into object literals ( ...) [ES2018]
> {...undefined}
{}
> {...null}
{}
> {...123}
{}
> {...'abc'}
{ '0': 'a', '1': 'b', '2': 'c' }
> {...['a', 'b']}
{ '0': 'a', '1': 'b' }
Property .length of strings and Arrays is hidden from this kind of operation
Spreading includes properties whose keys are symbols (which are ignored by
then those are not copied themselves; they are shared between original and
The first level of copy is really a copy: If we change any properties at that
copy.a = 2;
assert.deepEqual(
original, { a: 1, b: {prop: true} }); // no change
However, deeper levels are not copied. For example, the value of .b is shared
between original and copy. Changing .b in the copy also changes it in the
original.
copy.b.prop = false;
assert.deepEqual(
original, { a: 1, b: {prop: false} });
objects. Options:
Implement it ourselves.
structuredClone().
30.4.2 Use case for spreading: default values for missing properties
If one of the inputs of our code is an object with data, we can make
properties are missing. One technique for doing so is via an object whose
properties contain the default values. In the following example, that object is
DEFAULTS:
The result, the object allData, is created by copying DEFAULTS and overriding its
object: We set it (line A) and mutate the object. That is, this way of changing
a property is destructive.
(fixed key)
exercises/objects/update_name_test.mjs
[ES6]
30.4.4 “Destructive spreading”: Object.assign()
This expression assigns all properties of source_1 to target, then all properties
const target = { a: 1 };
assert.deepEqual(
result, { a: 1, b: true, c: 3 });
// target was modified and returned:
assert.equal(result, target);
The use cases for Object.assign() are similar to those for spread properties. In
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
},
};
const jane = {
first: 'Jane',
says: function (text) {
return `${this.first} says “${text}”`;
},
};
const obj = {
someMethod(x, y) {
assert.equal(this, obj); // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');
}
};
obj.someMethod('a', 'b'); // (B)
(line A).
Methods are functions and functions have methods themselves. One of those
works.
obj.someMethod('a', 'b')
.call() makes the normally implicit parameter this explicit: When invoking a
function via .call(), the first parameter is this, followed by the regular
As an aside, this means that there are actually two different dot operators:
They are different in that (2) is not just (1) followed by the function call
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`; // (A)
},
};
const func = jane.says.bind(jane, 'hello');
assert.equal(func(), 'Jane says “hello”');
Setting this to jane via .bind() is crucial here. Otherwise, func() wouldn’t
work properly because this is used in line A. In the next section, we’ll
We now know quite a bit about functions and methods and are ready to take
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
},
};
const func = jane.says; // extract the method
assert.throws(
() => func('hello'), // (A)
{
name: 'TypeError',
message: "Cannot read properties of undefined
(reading 'first')",
});
calls, this is undefined (if strict mode is active, which it almost always is).
assert.throws(
() => jane.says.call(undefined, 'hello'), // `this`
is undefined!
{
name: 'TypeError',
message: "Cannot read properties of undefined
(reading 'first')",
}
);
The .bind() ensures that this is always jane when we call func().
development:
class ClickHandler {
constructor(id, elem) {
this.id = id;
elem.addEventListener('click', this.handleClick);
// (A)
}
handleClick(event) {
alert('Clicked ' + this.id);
}
}
should do:
// Later, possibly:
elem.removeEventListener('click', listener);
exercises/objects/method_extraction_exrc.mjs
functions
we can’t access the this of the surrounding scope because the ordinary
function has its own this. In other words, a variable in an inner scope hides a
an example:
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x; // (A)
});
},
};
assert.throws(
() => prefixer.prefixStringArray(['a', 'b']),
{
name: 'TypeError',
message: "Cannot read properties of undefined
(reading 'prefix')",
}
);
since the surrounding ordinary function has its own this that shadows (and
blocks access to) the this of the method. The value of the former this is
undefined due to the callback being function-called. That explains the error
message.
The simplest way to fix this problem is via an arrow function, which doesn’t
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
(x) => {
return this.prefix + x;
});
},
};
assert.deepEqual(
prefixer.prefixStringArray(['a', 'b']),
['==> a', '==> b']);
We can also store this in a different variable (line A), so that it doesn’t get
shadowed:
prefixStringArray(stringArray) {
const that = this; // (A)
return stringArray.map(
function (x) {
return that.prefix + x;
});
},
Another option is to specify a fixed this for the callback via .bind() (line A):
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
}.bind(this)); // (A)
},
Lastly, .map() lets us specify a value for this (line A) that it uses when
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
},
this); // (A)
},
Inside a callable entity, the value of this depends on how the callable entity
Function call:
this)
I like to do that because top-level this is confusing and there are better
[ES2020]
(advanced)
If the value before the question mark is neither undefined nor null, then
first examples:
> null?.prop
undefined
> {prop: 1}?.prop
1
> null?.(123)
undefined
> String?.(123)
'123'
with a dot (.?) or a question mark (?.)? Then this mnemonic may help
you:
const persons = [
{
surname: 'Zoe',
address: {
street: {
name: 'Sesame Street',
number: '123',
},
},
},
{
surname: 'Mariner',
},
{
surname: 'Carmen',
address: {
},
},
];
instead of undefined:
o?.prop
(o !== undefined && o !== null) ? o.prop : undefined
Examples:
assert.equal(undefined?.prop, undefined);
assert.equal(null?.prop, undefined);
assert.equal({prop:1}?.prop, 1);
o?.[«expr»]
(o !== undefined && o !== null) ? o[«expr»] :
undefined
Examples:
f?.(arg0, arg1)
(f !== undefined && f !== null) ? f(arg0, arg1) :
undefined
Examples:
assert.equal(undefined?.(123), undefined);
assert.equal(null?.(123), undefined);
assert.equal(String?.(123), '123');
Note that this operator produces an error if its left-hand side is not callable:
assert.throws(
() => true?.(123),
TypeError);
Why? The idea is that the operator only tolerates deliberate omissions. An
uncallable value (other than undefined and null) is probably an error and
should be reported, rather than worked around.
the first optional operator encounters undefined or null at its left-hand side:
function invokeM(value) {
return value?.a.b.m(); // (A)
}
const obj = {
a: {
b: {
m() { return 'result' }
}
}
};
assert.equal(
invokeM(obj), 'result'
);
assert.equal(
invokeM(undefined), undefined // (B)
);
returns undefined.
This behavior differs from a normal operator where JavaScript always
surface much later and are then harder to debug. For example, a typo
single location:
Or we can write a function whose input is deeply nested data and whose
Further reading:
“Overly defensive programming” by Carl Vitullo
The syntaxes of the following two optional operator are not ideal:
Alas, the less elegant syntax is necessary because distinguishing the ideal
too complicated:
The operator ?. is mainly about its right-hand side: Does property .prop
exist? If not, stop early. Therefore, keeping information about its left-hand
side is rarely useful. However, only having a single “early termination” value
Objects work best as fixed-layout objects. But before ES6, JavaScript did not
have a data structure for dictionaries (ES6 brought Maps). Therefore, objects
had to be used as dictionaries, which imposed a signficant constraint:
Dictionary keys had to be strings (symbols were also introduced with ES6).
We first look at features of objects that are related to dictionaries but also
useful for fixed-layout objects. This section concludes with tips for actually
So far, we have always used fixed-layout objects. Property keys were fixed
const obj = {
mustBeAnIdentifier: 123,
};
// Get property
assert.equal(obj.mustBeAnIdentifier, 123);
// Set property
obj.mustBeAnIdentifier = 'abc';
assert.equal(obj.mustBeAnIdentifier, 'abc');
As a next step, we’ll go beyond this limitation for property keys: In this
subsection, we’ll use arbitrary fixed strings as keys. In the next subsection,
const obj = {
'Can be any string!': 123,
};
Second, when getting or setting properties, we can use square brackets with
// Get property
assert.equal(obj['Can be any string!'], 123);
// Set property
obj['Can be any string!'] = 'abc';
assert.equal(obj['Can be any string!'], 'abc');
const obj = {
'A nice method'() {
return 'Yes!';
},
};
const obj = {
['Hello world!']: true,
['p'+'r'+'o'+'p']: 123,
[Symbol.toStringTag]: 'Goodbye', // (A)
};
The main use case for computed keys is having symbols as property keys
(line A).
Note that the square brackets operator for getting and setting properties
assert.equal(obj['p'+'r'+'o'+'p'], 123);
assert.equal(obj['==> prop'.slice(4)], 123);
assert.equal(obj[methodKey](), 'Yes!');
For the remainder of this chapter, we’ll mostly use fixed property keys again
(because they are syntactically more convenient). But all features are also
(computed key)
exercises/objects/update_property_test.mjs
const obj = {
alpha: 'abc',
beta: false,
};
assert.equal('alpha' in obj, true);
assert.equal('beta' in obj, true);
assert.equal('unknownKey' in obj, false);
assert.equal(
obj.alpha ? 'exists' : 'does not exist',
'exists');
assert.equal(
obj.unknownKey ? 'exists' : 'does not exist',
'does not exist');
The previous checks work because obj.alpha is truthy and because reading a
There is, however, one important caveat: truthiness checks fail if the property
exists, but has a falsy value (undefined, null, false, 0, "", etc.):
assert.equal(
obj.beta ? 'exists' : 'does not exist',
'does not exist'); // should be: 'exists'
assert.deepEqual(Object.keys(obj), ['myProp']);
delete obj.myProp;
assert.deepEqual(Object.keys(obj), []);
30.7.5 Enumerability
const enumerableSymbolKey =
Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
Object.keys() ✔ ✔
Object.getOwnPropertyNames() ✔ ✔ ✔
Object.getOwnPropertySymbols() ✔ ✔ ✔
Reflect.ownKeys() ✔ ✔ ✔ ✔
property keys. All of them return Arrays with strings and/or symbols.
Each of the methods in table 30.1 returns an Array with the own property
keys of the parameter. In the names of the methods, we can see that the
To demonstrate the four operations, we revisit the example from the previous
subsection:
const enumerableSymbolKey =
Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
const obj = {
enumerableStringKey: 1,
[enumerableSymbolKey]: 2,
}
Object.defineProperties(obj, {
nonEnumStringKey: {
value: 3,
enumerable: false,
},
[nonEnumSymbolKey]: {
value: 4,
enumerable: false,
},
});
assert.deepEqual(
Object.keys(obj),
['enumerableStringKey']
);
assert.deepEqual(
Object.getOwnPropertyNames(obj),
['enumerableStringKey', 'nonEnumStringKey']
);
assert.deepEqual(
Object.getOwnPropertySymbols(obj),
[enumerableSymbolKey, nonEnumSymbolKey]
);
assert.deepEqual(
Reflect.ownKeys(obj),
[
'enumerableStringKey', 'nonEnumStringKey',
enumerableSymbolKey, nonEnumSymbolKey,
]
);
of an object:
[ES2017]
30.7.8 Listing property entries via Object.entries()
Object.entries(obj) returns an Array with one key-value pair for each of its
properties:
function entries(obj) {
return Object.keys(obj)
.map(key => [key, obj[key]]);
}
Exercise: Object.entries()
exercises/objects/find_key_test.mjs
30.7.9 Properties are listed deterministically
order:
1. Properties with string keys that contain integer indices (that includes
Array indices):
The following example demonstrates that property keys are sorted according
to these rules:
are ordered.
[ES2019]
30.7.10 Assembling objects via Object.fromEntries()
object:
const symbolKey = Symbol('symbolKey');
assert.deepEqual(
Object.fromEntries(
[
['stringKey', 1],
[symbolKey, 2],
]
),
{
stringKey: 1,
[symbolKey]: 2,
}
);
To demonstrate both, we’ll use them to implement two tool functions from
pick(object, ...keys)
It returns a copy of object that has only those properties whose keys are
invert(object)
It returns a copy of object where the keys and values of all properties are
swapped:
assert.deepEqual(
invert({a: 1, b: 2, c: 3}),
{1: 'a', 2: 'b', 3: 'c'}
);
function invert(object) {
const reversedEntries = Object.entries(object)
.map(([key, value]) => [value, key]);
return Object.fromEntries(reversedEntries);
}
function fromEntries(iterable) {
const result = {};
for (const [key, value] of iterable) {
let coercedKey;
if (typeof key === 'string' || typeof key ===
'symbol') {
coercedKey = key;
} else {
coercedKey = String(key);
}
result[coercedKey] = value;
}
return result;
}
exercises/objects/omit_properties_test.mjs
The first pitfall is that the in operator also finds inherited properties:
We want dict to be treated as empty, but the in operator detects the properties
The second pitfall is that we can’t use the property key __proto__ because it
dict['__proto__'] = 123;
// No property was added to dict:
assert.deepEqual(Object.keys(dict), []);
If we can, we use Maps. They are the best solution for dictionaries.
dictionaries:
dict['__proto__'] = 123;
assert.deepEqual(Object.keys(dict), ['__proto__']);
(line A).
exercises/objects/simple_dict_test.mjs
[ES5]
30.8 Property attributes and property descriptors
(advanced)
attributes. There are two kinds of properties and they are characterized by
their attributes:
A data property stores data. Its attribute value holds any JavaScript
value.
function. The former is stored in the attribute get, the latter in the
attribute set.
Additionally, there are attributes that both kinds of properties have. The
We have already encountered the attributes value, get, and set. The other
it is false, then:
When we are using one of the operations for handling property attributes,
attributes are specified via property descriptors: objects where each property
represents one attribute. For example, this is how we read the attributes of a
property obj.myProp:
const obj = { myProp: 123 };
assert.deepEqual(
Object.getOwnPropertyDescriptor(obj, 'myProp'),
{
value: 123,
writable: true,
enumerable: true,
configurable: true,
});
assert.deepEqual(Object.keys(obj), ['myProp']);
assert.deepEqual(Object.keys(obj), []);
const obj = {
myMethod() {},
get myGetter() {},
};
const propDescs =
Object.getOwnPropertyDescriptors(obj);
propDescs.myMethod.value = typeof
propDescs.myMethod.value;
propDescs.myGetter.get = typeof
propDescs.myGetter.get;
assert.deepEqual(
propDescs,
{
myMethod: {
value: 'function',
writable: true,
enumerable: true,
configurable: true
},
myGetter: {
get: 'function',
set: undefined,
enumerable: true,
configurable: true
}
}
);
Further reading
For more information on property attributes and property descriptors,
[ES5]
30.9 Protecting objects from being changed (advanced)
object and to change its prototype. We can still delete and change
properties, though.
Apply: Object.preventExtensions(obj)
Check: Object.isExtensible(obj)
Apply: Object.seal(obj)
Check: Object.isSealed(obj)
That is, the object is not extensible, all properties are read-only and
Apply: Object.freeze(obj)
Check: Object.isFrozen(obj)
All three of the aforementioned Object.* methods only affect the top
Further reading
prototype that is either null or an object. In the latter case, the object inherits
In an object literal, we can set the prototype via the special property
__proto__:
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};
Given that a prototype object can have a prototype itself, we get a chain of
that we are dealing with single objects, but we are actually dealing with
chains of objects.
Figure 30.2 shows what the prototype chain of obj looks like.
...
proto
__proto__
protoProp 'a'
obj
__proto__
objProp 'b'
Figure 30.2: obj starts a chain of objects that continues with proto and
other objects.
Non-inherited properties are called own properties. obj has one own
property, .objProp.
Some operations consider all properties (own and inherited) – for example,
getting properties:
> Object.keys(obj)
[ 'one' ]
Read on for another operation that also only considers own properties:
setting properties.
Given an object obj with a chain of prototype objects, it makes sense that
inherited property via obj also only changes obj. It creates a new own
property in obj that overrides the inherited property. Let’s explore how that
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};
In the next code snippet, we set the inherited property obj.protoProp (line A).
the own property is found first and its value overrides the value of the
inherited property.
...
proto
__proto__
protoProp 'a'
obj
__proto__
objProp 'b'
protoProp 'x'
Figure 30.3: The own property .protoProp of obj overrides the property
Object):
It can’t be used with all objects (e.g. objects that are not instances
of Object).
(accessor)” (§31.8.8).
The best time to set the prototype of an object is when we are creating
const obj1 = {
__proto__: proto1,
};
assert.equal(Object.getPrototypeOf(obj1), proto1);
const obj2 = Object.create(proto2a);
assert.equal(Object.getPrototypeOf(obj2), proto2a);
Object.setPrototypeOf(obj2, proto2b);
assert.equal(Object.getPrototypeOf(obj2), proto2b);
of obj”. But it can also be used more loosely and mean that proto is in the
.isPrototypeOf():
For example:
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert.equal(c.isPrototypeOf(a), false);
assert.equal(a.isPrototypeOf(a), false);
(§31.8.6).
[ES2022]
30.10.4 Object.hasOwn(): Is a given property own (non-inherited)?
The in operator (line A) checks if an object has a given property. In contrast,
const proto = {
protoProp: 'protoProp',
};
const obj = {
__proto__: proto,
objProp: 'objProp',
}
assert.equal('protoProp' in obj, true); // (A)
assert.equal(Object.hasOwn(obj, 'protoProp'), false);
// (B)
assert.equal(Object.hasOwn(proto, 'protoProp'),
true); // (C)
We have two objects that are very similar. Both have two properties whose
const PersonProto = {
describe() {
return 'Person named ' + this.firstName;
},
};
const jane = {
__proto__: PersonProto,
firstName: 'Jane',
};
const tarzan = {
__proto__: PersonProto,
firstName: 'Tarzan',
};
The name of the prototype reflects that both jane and tarzan are persons.
PersonProto
describe function() {···}
jane tarzan
__proto__ __proto__
firstName 'Jane' firstName 'Tarzan'
Figure 30.4: Objects jane and tarzan share method .describe(), via their
Figure 30.4 illustrates how the three objects are connected: The objects at the
bottom now contain the properties that are specific to jane and tarzan. The
object at the top contains the properties that are shared between them.
When we make the method call jane.describe(), this points to the receiver of
that method call, jane (in the bottom-left corner of the diagram). That’s why
Looking ahead to the next chapter on classes – this is how classes are
organized internally:
In principle, objects are unordered. The main reason for ordering properties
is so that operations that list entries, keys, or values are deterministic. That
[ES5]
Object.create(proto, propDescObj?)
parameter:
Sets the prototype of obj to proto (which must be null or an object) and
[ES5]
Object.defineProperty(obj, propKey, propDesc)
Defines one property in obj, as specified by the property key propKey
Returns obj.
[ES5]
Object.defineProperties(obj, propDescObj)
property descriptors.
Returns obj.
property of obj.
[ES5]
Object.keys(obj)
Returns an Array with all own enumerable property keys that are
strings.
Returns an Array with all own property keys that are strings
Returns an Array with all own property keys that are symbols
properties.
[ES2017]
Object.entries(obj)
const obj = {
a: 1,
b: 2,
[Symbol('myKey')]: 3,
};
assert.deepEqual(
Object.entries(obj),
[
['a', 1],
['b', 2],
// Property with symbol key is ignored
]
);
[ES2019]
Object.fromEntries(keyValueIterable)
Creates an object whose own properties are specified by
keyValueIterable.
[ES2022]
Object.hasOwn(obj, key)
Returns true if obj has an own property whose key is key. If not, it
returns false.
[ES5]
Object.preventExtensions(obj)
Effect:
prototype.
Related: Object.isExtensible()
[ES5]
Object.isExtensible(obj)
Related: Object.preventExtensions()
[ES5]
Object.seal(obj)
Effect:
prototype.
unconfigurable.
Related: Object.isSealed()
[ES5]
Object.isSealed(obj)
Related: Object.seal()
[ES5]
Object.freeze(obj)
Effect:
prototype.
unconfigurable.
obj is frozen: Additionally, all of its properties are non-
writable.
Related: Object.isFrozen()
[ES5]
Object.isFrozen(obj)
Related: Object.freeze()
[ES6]
Object.assign(target, ...sources)
Assigns all enumerable own string-keyed properties of each of the
items.
its value is an Array with all items that have that group key.
assert.deepEqual(
Object.groupBy(
['orange', 'apricot', 'banana', 'apple',
'blueberry'],
(str) => str[0] // compute group key
),
{
__proto__: null,
'o': ['orange'],
'a': ['apricot', 'apple'],
'b': ['banana', 'blueberry'],
}
);
[ES6]
Object.is(value1, value2)
> -0 === 0
true
> Object.is(-0, 0)
false
The value -0 is rare and it’s usually best to pretend it is the same as
0.
30.12.6 Object.prototype.*
Object.prototype.hasOwnProperty()
Object.prototype.isPrototypeOf()
Object.prototype.propertyIsEnumerable()
Object.prototype.toLocaleString()
Object.prototype.toString()
Object.prototype.valueOf()
Object.prototype.*” (§31.8.1).
useful elsewhere:
succeeded.
Reflect.deleteProperty(target, propertyKey)
It returns false if the property could not be deleted and still exists.
In sloppy mode, the delete operator returns the same results as this
false.
non-configurable.
Reflect.getOwnPropertyDescriptor(target, propertyKey)
Same as Object.getOwnPropertyDescriptor().
Reflect.getPrototypeOf(target)
Same as Object.getPrototypeOf().
Reflect.has(target, propertyKey)
Reflect.isExtensible(target)
Same as Object.isExtensible().
Reflect.ownKeys(target)
Reflect.preventExtensions(target)
Similar to Object.preventExtensions().
succeeded.
Sets properties.
succeeded.
Reflect.setPrototypeOf(target, proto)
Same as Object.setPrototypeOf().
succeeded.
Use Reflect.* when working with ECMAScript proxies. Its methods are
Object.preventExtensions(obj)
Object.setPrototypeOf(obj, proto)
Reflect.deleteProperty(target, propertyKey)
Reflect.has(target, propertyKey)
dispatch, because the function may have an own property with the key
'apply':
[ES2022]
31.2.5 Private slots in more detail (advanced)
[ES2022]
31.4.2 Private methods and accessors
[ES2022]
31.5 Instance members of classes
WeakMaps (advanced)
[ES2022]
31.6.2 Static public fields
[ES2022]
31.6.3 Static private methods, accessors, and fields
[ES2022]
31.6.4 Static initialization blocks in classes
private members
31.7 Subclassing
31.8.3 Object.prototype.toString()
31.8.4 Object.prototype.toLocaleString()
31.8.5 Object.prototype.valueOf()
31.8.6 Object.prototype.isPrototypeOf()
31.8.7 Object.prototype.propertyIsEnumerable()
31.8.9 Object.prototype.hasOwnProperty()
31.9 FAQ: classes
31.9.1 Why are they called “instance private fields” in this book
introduced in four steps. This chapter covers step 3 and 4, the previous
mechanism.
SuperClass
superData
superMthd
mthd ƒ
MyClass SubClass
mthd ƒ __proto__ data subData
data 4 data 4 mthd subMthd
A JavaScript class:
class Person {
constructor(firstName) { // (A)
this.firstName = firstName; // (B)
}
describe() { // (C)
return 'Person named ' + this.firstName;
}
}
const tarzan = new Person('Tarzan');
assert.equal(
tarzan.firstName, 'Tarzan'
);
assert.equal(
tarzan.describe(),
'Person named Tarzan'
);
// One property (public slot)
assert.deepEqual(
Reflect.ownKeys(tarzan), ['firstName']
);
Explanations:
declaration necessary).
class Person {
#firstName; // (A)
constructor(firstName) {
this.#firstName = firstName; // (B)
}
describe() {
return 'Person named ' + this.#firstName;
}
}
const tarzan = new Person('Tarzan');
assert.equal(
tarzan.describe(),
'Person named Tarzan'
);
// No properties, only a private field
assert.deepEqual(
Reflect.ownKeys(tarzan), []
);
Explanations:
must be declared (line A) before they can be used (line B). A private
field can only be accessed inside the class that declares it. It can’t even
be accessed by subclasses.
constructor(firstName, title) {
super(firstName); // (A)
this.#title = title;
}
describe() {
return `${super.describe()} (${this.#title})`;
// (B)
}
}
The next class demonstrates how to create properties via public fields (line
A):
class StringBuilderClass {
string = ''; // (A)
add(str) {
this.string += str;
return this;
}
}
(which are explained in the previous chapter). Under the hood, JavaScript’s
working with them. They should normally feel familiar to people who have
Note that we don’t need classes to create objects. We can also do so via
object literals. That’s why the singleton pattern isn’t needed in JavaScript
and classes are used less than in many other languages that have them.
We have previously worked with jane and tarzan, single objects representing
objects:
class Person {
#firstName; // (A)
constructor(firstName) {
this.#firstName = firstName; // (B)
}
describe() {
return `Person named ${this.#firstName}`;
}
static extractNames(persons) {
return persons.map(person => person.#firstName);
}
}
jane and tarzan can now be created via new Person():
[ES2022]
.#firstName is an instance private field: Such fields are stored in
are separate – they always start with hash symbols (#). And they are
assert.deepEqual(
Reflect.ownKeys(jane),
[]
);
assert.equal(
jane.describe(), 'Person named Jane'
);
assert.equal(
tarzan.describe(), 'Person named Tarzan'
);
assert.deepEqual(
Person.extractNames([jane, tarzan]),
['Jane', 'Tarzan']
);
class Container {
constructor(value) {
this.value = value;
}
}
const abcContainer = new Container('abc');
assert.equal(
abcContainer.value, 'abc'
);
class and stays the same, regardless of what the class is assigned to.
looked at subclassing.
Public slots (which are are also called properties). For example,
[ES2022]
Private slots . For example, private fields are private slots.
These are the most important rules we need to know about properties and
private slots:
getters and setters. All of them are slots in objects. Which objects they
are placed in depends on whether the keyword static is used and other
factors.
A getter and a setter that have the same key create a single accessor
Their keys are different. The keys of private slots can’t even be
The following class demonstrates the two kinds of slots. Each of its
assert.deepEqual(
Reflect.ownKeys(inst),
['instanceProperty']
);
This chapter doesn’t cover all details of properties (just the essentials).
[ES2022]
31.2.5 Private slots in more detail (advanced)
A private slot really can only be accessed inside the class that declares it.
class SuperClass {
#superProp = 'superProp';
}
class SubClass extends SuperClass {
getSuperProp() {
return this.#superProp;
}
}
// SyntaxError: Private field '#superProp'
// must be declared in an enclosing class
Private slots have unique keys that are similar to symbols. Consider the
let MyClass;
{ // Scope of the body of the class
const instancePrivateFieldKey = Symbol();
MyClass = class {
__PrivateElements__ = new Map([
[instancePrivateFieldKey, 1],
]);
instanceProperty = 2;
getInstanceValues() {
return [
this.__PrivateElements__.get(instancePrivateFieldKey
),
this.instanceProperty,
];
}
}
}
private names directly in JavaScript, we can only use them indirectly, via
the fixed identifiers of private fields, private methods, and private accessors.
refer to values).
A callable entity can only access the name of a private slot if it was born
inside the scope where the name was declared. However, it doesn’t lose this
class MyClass {
#privateData = 'hello';
static createGetter() {
return (obj) => obj.#privateData; // (A)
}
}
The arrow function getter was born inside MyClass (line A), but it can still
access the private name #privateData after it left its birth scope (line B).
31.2.5.4 The same private identifier refers to different private names in different classes
Because the identifiers of private slots aren’t used as keys, using the same
identifier in different classes produces different slots (line A and line C):
class Color {
#name; // (A)
constructor(name) {
this.#name = name; // (B)
}
static getName(obj) {
return obj.#name;
}
}
class Person {
#name; // (C)
constructor(name) {
this.#name = name;
}
}
assert.equal(
Color.getName(new Color('green')), 'green'
);
Even if a subclass uses the same name for a private field, the two names
never clash because they refer to private names (which are always unique).
.#privateField in SubClass, even though both slots are stored directly in inst:
class SuperClass {
#privateField = 'super';
getSuperPrivateField() {
return this.#privateField;
}
}
class SubClass extends SuperClass {
#privateField = 'sub';
getSubPrivateField() {
return this.#privateField;
}
}
const inst = new SubClass();
assert.equal(
inst.getSuperPrivateField(), 'super'
);
assert.equal(
inst.getSubPrivateField(), 'sub'
);
The in operator can be used to check if a private slot exists (line A):
class Color {
#name;
constructor(name) {
this.#name = name;
}
static check(obj) {
return #name in obj; // (A)
}
}
Private methods. The following code shows that private methods create
class C1 {
#priv() {}
static check(obj) {
return #priv in obj;
}
}
assert.equal(C1.check(new C1()), true);
Static private fields. We can also use in for a static private field:
class C2 {
static #priv = 1;
static check(obj) {
return #priv in obj;
}
}
assert.equal(C2.check(C2), true);
assert.equal(C2.check(new C2()), false);
Static private methods. And we can check for the slot of a static private
method:
class C3 {
static #priv() {}
static check(obj) {
return #priv in obj;
}
}
assert.equal(C3.check(C3), true);
Using the same private identifier in different classes. In the next example,
the two classes Color and Person both have a slot whose identifier is #name. The
class Color {
#name;
constructor(name) {
this.#name = name;
}
static check(obj) {
return #name in obj;
}
}
class Person {
#name;
constructor(name) {
this.#name = name;
}
static check(obj) {
return #name in obj;
}
}
Classes are a common standard for object creation and inheritance that
They help tools such as IDEs and type checkers with their work and
JavaScript engines optimize them. That is, code that uses classes is
almost always faster than code that uses a custom inheritance library.
but they work differently and are used differently (see next subsection).
C.prototype.
Classes are functions.
This was a first look at classes. We’ll explore more features soon.
exercises/classes/point_class_test.mjs
class Person {
#firstName;
constructor(firstName) {
this.#firstName = firstName;
}
describe() {
return `Person named ${this.#firstName}`;
}
static extractNames(persons) {
return persons.map(person => person.#firstName);
}
}
The first object created by the class is stored in Person. It has four properties:
assert.deepEqual(
Reflect.ownKeys(Person),
['length', 'name', 'prototype', 'extractNames']
);
action.
definition.
assert.deepEqual(
Reflect.ownKeys(Person.prototype),
['constructor', 'describe']
);
That explains how the instances get their methods: They inherit them from
Person Person.prototype
prototype constructor
extractNames function() {···} describe function() {···}
jane tarzan
__proto__ __proto__
#firstName 'Jane' #firstName 'Tarzan'
Figure 31.2: The class Person has the property .prototype that points to
gets and sets the prototype of the receiver. Therefore the following two
someObj.__proto__
Object.getPrototypeOf(someObj)
someObj.__proto__ = anotherObj
Object.setPrototypeOf(someObj, anotherObj)
class SomeClass {}
const inst = new SomeClass();
assert.equal(
Object.getPrototypeOf(inst),
SomeClass.prototype
);
This setup exists due to backward compatibility. But it has two additional
benefits.
Second, we can get the name of the class that created a given instance:
methods work.
We’ll also need the second way later in this chapter: It will allow us to
Let’s examine how method calls work with classes. We are revisiting jane
from earlier:
class Person {
#firstName;
constructor(firstName) {
this.#firstName = firstName;
}
describe() {
return 'Person named '+this.#firstName;
}
}
const jane = new Person('Jane');
Person.prototype
__proto__
describe function() {···}
jane
__proto__
#firstName 'Jane'
Figure 31.3: The prototype chain of jane starts with jane and continues
with Person.prototype.
jane.describe()
find the first object that has an own property with the key 'describe': It
invoking a value in that it not only calls what comes before the
parentheses with the arguments inside the parentheses but also sets this
dynamic dispatch.
Person.prototype.describe.call(jane)
don’t search for it in the prototype chain. We also specify this differently –
via .call().
example.
method from elsewhere that a given object doesn’t have – for example:
function StringBuilderConstr(initialString) {
this.string = initialString;
}
StringBuilderConstr.prototype.add = function (str) {
this.string += str;
return this;
};
class StringBuilderClass {
constructor(initialString) {
this.string = initialString;
}
add(str) {
this.string += str;
return this;
}
}
const sb = new StringBuilderClass('¡');
sb.add('Hola').add('!');
assert.equal(
sb.string, '¡Hola!'
);
And more.
Classes are so compatible with constructor functions that they can even
extend them:
function SuperConstructor() {}
class SubClass extends SuperConstructor {}
assert.equal(
new SubClass() instanceof SuperConstructor, true
);
Due to how similar they are, I use the terms constructor (function) and
class interchangeably.
All members in the body of the following class declaration create properties
of PublicProtoClass.prototype.
class PublicProtoClass {
constructor(args) {
// (Do something with `args` here.)
}
publicProtoMethod() {
return 'publicProtoMethod';
}
get publicProtoAccessor() {
return 'publicProtoGetter';
}
set publicProtoAccessor(value) {
assert.equal(value, 'publicProtoSetter');
}
}
assert.deepEqual(
Reflect.ownKeys(PublicProtoClass.prototype),
['constructor', 'publicProtoMethod',
'publicProtoAccessor']
);
class PublicProtoClass2 {
// Identifier keys
get accessor() {}
set accessor(value) {}
syncMethod() {}
* syncGeneratorMethod() {}
async asyncMethod() {}
async * asyncGeneratorMethod() {}
// Quoted keys
get 'an accessor'() {}
set 'an accessor'(value) {}
'sync method'() {}
* 'sync generator method'() {}
async 'async method'() {}
async * 'async generator method'() {}
// Computed keys
get [accessorKey]() {}
set [accessorKey](value) {}
[syncMethodKey]() {}
* [syncGenMethodKey]() {}
async [asyncMethodKey]() {}
async * [asyncGenMethodKey]() {}
}
[ES2022]
31.4.2 Private methods and accessors
On one hand, private methods are stored in slots in instances (line A):
class MyClass {
#privateMethod() {}
static check() {
const inst = new MyClass();
assert.equal(
#privateMethod in inst, true // (A)
);
assert.equal(
#privateMethod in MyClass.prototype, false
);
assert.equal(
#privateMethod in MyClass, false
);
}
}
MyClass.check();
Why are they not stored in .prototype objects? Private slots are not inherited,
On the other hand, private methods are shared between instances – like
class MyClass {
#privateMethod() {}
static check() {
const inst1 = new MyClass();
const inst2 = new MyClass();
assert.equal(
inst1.#privateMethod,
inst2.#privateMethod
);
}
}
Due to that and due to their syntax being similar to prototype public
The following code demonstrates how private methods and accessors work:
class PrivateMethodClass {
#privateMethod() {
return 'privateMethod';
}
get #privateAccessor() {
return 'privateGetter';
}
set #privateAccessor(value) {
assert.equal(value, 'privateSetter');
}
callPrivateMembers() {
assert.equal(this.#privateMethod(),
'privateMethod');
assert.equal(this.#privateAccessor,
'privateGetter');
this.#privateAccessor = 'privateSetter';
}
}
assert.deepEqual(
Reflect.ownKeys(new PrivateMethodClass()), []
);
class PrivateMethodClass2 {
get #accessor() {}
set #accessor(value) {}
#syncMethod() {}
* #syncGeneratorMethod() {}
async #asyncMethod() {}
async * #asyncGeneratorMethod() {}
}
More information on accessors (defined via getters and/or setters),
[ES2022]
31.5 Instance members of classes
Instances of the following class have two instance properties (created in line
class InstPublicClass {
// Instance public field
instancePublicField = 0; // (A)
constructor(value) {
// We don’t need to mention .property elsewhere!
this.property = value; // (B)
}
}
more so than in, e.g., Java, where most instance state is private.
31.5.1.1 Instance public fields with quoted and computed keys (advanced)
instance:
class MyClass {
instancePublicField = this;
}
const inst = new MyClass();
assert.equal(
inst.instancePublicField, inst
);
The execution of instance public fields roughly follows these two rules:
class SuperClass {
superProp = console.log('superProp');
constructor() {
console.log('super-constructor');
}
}
class SubClass extends SuperClass {
subProp = console.log('subProp');
constructor() {
console.log('BEFORE super()');
super();
console.log('AFTER super()');
}
}
new SubClass();
Output:
BEFORE super()
superProp
super-constructor
subProp
AFTER super()
The following class contains two instance private fields (line A and line B):
class InstPrivateClass {
#privateField1 = 'private field 1'; // (A)
#privateField2; // (B) required!
constructor(value) {
this.#privateField2 = value; // (C)
}
/**
* Private fields are not accessible outside the
class body.
*/
checkPrivateValues() {
assert.equal(
this.#privateField1, 'private field 1'
);
assert.equal(
this.#privateField2, 'constructor argument'
);
}
}
class body.
Because they don’t rely on classes, we can also use them for objects that
The first technique makes a property private by prefixing its name with an
underscore. This doesn’t protect the property in any way; it merely signals
In the following code, the properties ._counter and ._action are private.
class Countdown {
constructor(counter, action) {
this._counter = counter;
this._action = action;
}
dec() {
this._counter--;
if (this._counter === 0) {
this._action();
}
}
}
Private methods work similarly: They are normal methods whose names
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
there can’t be any name clashes. But it is also more complicated to use.
who has access to it – for example: If the variable exists inside a module and
isn’t exported, everyone inside the module and no one outside the module
can access it. In other words: The scope of privacy isn’t the class in this
let Countdown;
{ // class scope
const _counter = new WeakMap();
const _action = new WeakMap();
Countdown = class {
// ···
}
}
functions that have access to _superProp are the next best thing:
Note that this becomes the explicit function parameter _this (line A).
WeakMaps (advanced)
As previously discussed, instance private fields are only visible inside their
classes and not even in subclasses. Thus, there is no built-in way to get:
Protected visibility: A class and all of its subclasses can access a piece
instance data.
Therefore:
If we put a class and its subclasses into the same module, we get
protected visibility.
If we put a class and its friends into the same module, we get friend
visibility.
All members in the body of the following class declaration create so-called
class StaticPublicMethodsClass {
static staticMethod() {
return 'staticMethod';
}
static get staticAccessor() {
return 'staticGetter';
}
static set staticAccessor(value) {
assert.equal(value, 'staticSetter');
}
}
assert.equal(
StaticPublicMethodsClass.staticMethod(),
'staticMethod'
);
assert.equal(
StaticPublicMethodsClass.staticAccessor,
'staticGetter'
);
StaticPublicMethodsClass.staticAccessor =
'staticSetter';
class StaticPublicMethodsClass2 {
// Identifier keys
static get accessor() {}
static set accessor(value) {}
static syncMethod() {}
static * syncGeneratorMethod() {}
static async asyncMethod() {}
static async * asyncGeneratorMethod() {}
// Quoted keys
static get 'an accessor'() {}
static set 'an accessor'(value) {}
static 'sync method'() {}
static * 'sync generator method'() {}
static async 'async method'() {}
static async * 'async generator method'() {}
// Computed keys
static get [accessorKey]() {}
static set [accessorKey](value) {}
static [syncMethodKey]() {}
static * [syncGenMethodKey]() {}
static async [asyncMethodKey]() {}
static async * [asyncGenMethodKey]() {}
}
[ES2022]
31.6.2 Static public fields
assert.deepEqual(
Reflect.ownKeys(StaticPublicFieldClass),
[
'length', // number of constructor parameters
'name', // 'StaticPublicFieldClass'
'prototype',
'identifierFieldKey',
'quoted field key',
computedFieldKey,
],
);
assert.equal(StaticPublicFieldClass.identifierFieldK
ey, 1);
assert.equal(StaticPublicFieldClass['quoted field
key'], 2);
assert.equal(StaticPublicFieldClass[computedFieldKey
], 3);
[ES2022]
31.6.3 Static private methods, accessors, and fields
The following class has two static private slots (line A and line B):
class StaticPrivateClass {
// Declare and initialize
static #staticPrivateField = 'hello'; // (A)
static #twice() { // (B)
const str =
StaticPrivateClass.#staticPrivateField;
return str + ' ' + str;
}
static getResultOfTwice() {
return StaticPrivateClass.#twice();
}
}
assert.deepEqual(
Reflect.ownKeys(StaticPrivateClass),
[
'length', // number of constructor parameters
'name', // 'StaticPublicFieldClass'
'prototype',
'getResultOfTwice',
],
);
assert.equal(
StaticPrivateClass.getResultOfTwice(),
'hello hello'
);
class MyClass {
static #staticPrivateMethod() {}
static * #staticPrivateGeneratorMethod() {}
[ES2022]
31.6.4 Static initialization blocks in classes
instance is created
Static fields
class Translator {
static translations = {
yes: 'ja',
no: 'nein',
maybe: 'vielleicht',
};
static englishWords = [];
static germanWords = [];
static { // (A)
for (const [english, german] of
Object.entries(this.translations)) {
this.englishWords.push(english);
this.germanWords.push(german);
}
}
}
We could also execute the code inside the static block after the class (at the
The rules for how static initialization blocks work, are relatively simple:
field initializers.
members of a subclass.
class SuperClass {
static superField1 = console.log('superField1');
static {
assert.equal(this, SuperClass);
console.log('static block 1 SuperClass');
}
static superField2 = console.log('superField2');
static {
console.log('static block 2 SuperClass');
}
}
Output:
superField1
static block 1 SuperClass
superField2
static block 2 SuperClass
subField1
static block 1 SubClass
subField2
static block 2 SubClass
In static public members, we can access static public slots via this. Alas, we
class SuperClass {
static publicData = 1;
static getPublicViaThis() {
return this.publicData;
}
}
class SubClass extends SuperClass {
}
assert.equal(SuperClass.getPublicViaThis(), 1);
then this points to SuperClass and everything works as expected. We can also
assert.equal(SubClass.getPublicViaThis(), 1);
to SubClass and things continue to work, because SubClass also inherits the
property .publicData.
from SuperClass.
class SuperClass {
static #privateData = 2;
static getPrivateDataViaThis() {
return this.#privateData;
}
static getPrivateDataViaClassName() {
return SuperClass.#privateData;
}
}
class SubClass extends SuperClass {
}
SuperClass:
assert.equal(SuperClass.getPrivateDataViaThis(), 2);
because this now points to SubClass and SubClass has no static private field
assert.throws(
() => SubClass.getPrivateDataViaThis(),
{
name: 'TypeError',
message: 'Cannot read private member
#privateData from'
+ ' an object whose class did not declare it',
}
);
assert.equal(SubClass.getPrivateDataViaClassName(),
2);
31.6.6 All members (static, prototype, instance) can access all private
members
Every member inside a class can access all other members inside that class
class DemoClass {
static #staticPrivateField = 1;
#instPrivField = 2;
static staticMethod(inst) {
// A static method can access static private
fields
// and instance private fields
assert.equal(DemoClass.#staticPrivateField, 1);
assert.equal(inst.#instPrivField, 2);
}
protoMethod() {
// A prototype method can access instance
private fields
// and static private fields
assert.equal(this.#instPrivField, 2);
assert.equal(DemoClass.#staticPrivateField, 1);
}
}
The following code only works in ES2022 – due to every line that has a
class StaticClass {
static #secret = 'Rumpelstiltskin';
static #getSecretInParens() {
return `(${StaticClass.#secret})`;
}
static callStaticPrivateMethod() {
return StaticClass.#getSecretInParens();
}
}
Since private slots only exist once per class, we can move #secret and
class Point {
static fromPolar(radius, angle) {
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
return new Point(x, y);
}
constructor(x=0, y=0) {
this.x = x;
this.y = y;
}
}
assert.deepEqual(
Point.fromPolar(13, 0.39479111969976155),
new Point(12, 5)
);
I like how descriptive static factory methods are: fromPolar describes how an
Array.from()
Object.create()
One factory method will probably directly call the constructor (but
outside.
31.7 Subclassing
Classes can also extend existing classes. For example, the following class
Base class A
Class B extends A.
Class C extends B.
If we invoke new C(), C’s constructor super-calls B’s constructor which super-
calls A’s constructor. Instances are always created in base classes, before the
exist before we call super() and we can’t access it via this, yet.
Note that static public slots are inherited. For example, Employee inherits the
Exercise: Subclassing
exercises/classes/color_point_class_test.mjs
Function.prototype Object.prototype
__proto__ __proto__
prototype
Person Person.prototype
__proto__ __proto__
prototype
Employee Employee.prototype
__proto__
jane
Figure 31.4: These are the objects that make up class Person and its
subclass, Employee. The left column is about classes. The right column
several objects (figure 31.4). One key insight for understanding how these
The instance prototype chain starts with jane and continues with
at this point, but we get one more object: Object.prototype. This prototype
too:
In the class prototype chain, Employee comes first, Person next. Afterward, the
prototype chain of x. That is, the following two expressions are equivalent:
x instanceof C
C.prototype.isPrototypeOf(x)
If we go back to figure 31.4, we can confirm that the prototype chain does
Note that instanceof always returns false if its self-hand side is a primitive
value:
assert.equal(
{a: 1} instanceof Object, true
);
assert.equal(
['a'] instanceof Object, true
);
assert.equal(
/abc/g instanceof Object, true
);
assert.equal(
new Map() instanceof Object, true
);
class C {}
assert.equal(
new C() instanceof Object, true
);
In the next example, obj1 and obj2 are both objects (line A and line C), but
they are not instances of Object (line B and line D): Object.prototype is not in
Object.prototype is the object that ends most prototype chains. Its prototype is
chains of a few built-in objects. The following tool function p() helps us
const p = Object.getPrototypeOf.bind(Object);
We extracted method .getPrototypeOf() of Object and assigned it to p.
null
__proto__
Object.prototype
__proto__
{}
literal starts with that object, continues with Object.prototype, and ends
with null.
Figure 31.5 shows a diagram for this prototype chain. We can see that {}
null
__proto__
Object.prototype
__proto__
Array.prototype
__proto__
[]
Figure 31.6: The prototype chain of an Array has these members: the
This prototype chain (visualized in figure 31.6) tells us that an Array object
Lastly, the prototype chain of an ordinary function tells us that all functions
are objects:
> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
true
class A {}
assert.equal(
Object.getPrototypeOf(A),
Function.prototype
);
assert.equal(
Object.getPrototypeOf(class {}),
Function.prototype
);
class B extends A {}
assert.equal(
Object.getPrototypeOf(B),
A
);
assert.equal(
Object.getPrototypeOf(class extends Object {}),
Object
);
objects need.
Base classes are where instances are actually created. Both Array and
Function need to create their own instances because they have so-called
Object.
31.7.5 Mixin classes (advanced)
JavaScript’s class system only supports single inheritance. That is, each
class can have at most one superclass. One way around this limitation is via
The idea is as follows: Let’s say we want a class C to inherit from two
doesn’t support.
Each of these two functions returns a class that extends a given superclass
We now have a class C that extends the class returned by S2() which extends
to its superclass:
const Named = (Sup) => class extends Sup {
name = '(Unnamed)';
toString() {
const className = this.constructor.name;
return `${className} named ${this.name}`;
}
};
The same class can extend a single superclass and zero or more mixins.
almost all objects are instances of Object. This class provides useful
object?
Object.getPrototypeOf()
Object.setPrototypeOf()
given key?
Before we take a closer look at each of these features, we’ll learn about an
important pitfall (and how to work around it): We can’t use the features of
one hand, this method isn’t available if an object is not an instance of Object
const obj = {
hasOwnProperty: 'yes' // (A)
};
assert.throws(
() => obj.hasOwnProperty('prop'),
{
name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
}
);
How does this code work? In line A in the example before the code above,
we used the function method .call() to turn the function hasOwnProperty with
one implicit parameter (this) and one explicit parameter (propName) into a
receiver (this):
of f.
Etc.
We use .bind() to create a version .call() whose this always refers to
dispatch?
objects.
If, on the other hand, we don’t know their receivers and/or they are
31.8.3 Object.prototype.toString()
For converting objects to strings it’s better to use String() because that also
31.8.4 Object.prototype.toLocaleString()
and often additional options. Any class or instance can implement this
Array.prototype.toLocaleString()
Number.prototype.toLocaleString()
Date.prototype.toLocaleString()
TypedArray.prototype.toLocaleString()
BigInt.prototype.toLocaleString()
> 123.45.toLocaleString('fr')
'123,45'
> 123.45.toLocaleString('en')
'123.45'
31.8.5 Object.prototype.valueOf()
31.8.6 Object.prototype.isPrototypeOf()
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false);
This is how to use this method safely (for details see “Using Object.prototype
const obj = {
// Overrides Object.prototype.isPrototypeOf
isPrototypeOf: true,
};
// Doesn’t work in this case:
assert.throws(
() => obj.isPrototypeOf(Object.prototype),
{
name: 'TypeError',
message: 'obj.isPrototypeOf is not a function',
}
);
// Safe way of using .isPrototypeOf():
assert.equal(
Object.prototype.isPrototypeOf.call(obj,
Object.prototype), false
);
31.8.7 Object.prototype.propertyIsEnumerable()
const proto = {
enumerableProtoProp: true,
};
const obj = {
__proto__: proto,
enumerableObjProp: true,
nonEnumObjProp: true,
};
Object.defineProperty(
obj, 'nonEnumObjProp',
{
enumerable: false,
}
);
assert.equal(
obj.propertyIsEnumerable('enumerableProtoProp'),
false // not an own property
);
assert.equal(
obj.propertyIsEnumerable('enumerableObjProp'),
true
);
assert.equal(
obj.propertyIsEnumerable('nonEnumObjProp'),
false // not enumerable
);
assert.equal(
obj.propertyIsEnumerable('unknownProp'),
false // not a property
);
This is how to use this method safely (for details see “Using Object.prototype
const obj = {
// Overrides Object.prototype.propertyIsEnumerable
propertyIsEnumerable: true,
enumerableProp: 'yes',
};
// Doesn’t work in this case:
assert.throws(
() => obj.propertyIsEnumerable('enumerableProp'),
{
name: 'TypeError',
message: 'obj.propertyIsEnumerable is not a
function',
}
);
// Safe way of using .propertyIsEnumerable():
assert.equal(
Object.prototype.propertyIsEnumerable.call(obj,
'enumerableProp'),
true
);
created by them.
and “legacy”.
class Object {
get __proto__() {
return Object.getPrototypeOf(this);
}
set __proto__(other) {
Object.setPrototypeOf(this, other);
}
// ···
}
> '__proto__' in {}
true
> '__proto__' in Object.create(null)
false
31.8.9 Object.prototype.hasOwnProperty()
[ES2022]
Better alternative to .hasOwnProperty(): Object.hasOwn()
See “Object.hasOwn(): Is a given property own (non-inherited)?”
(§30.10.4).
This is how to use this method safely (for details see “Using Object.prototype
const obj = {
// Overrides Object.prototype.hasOwnProperty
hasOwnProperty: true,
};
// Doesn’t work in this case:
assert.throws(
() => obj.hasOwnProperty('anyPropKey'),
{
name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
}
);
// Safe way of using .hasOwnProperty():
assert.equal(
Object.prototype.hasOwnProperty.call(obj,
'anyPropKey'), false
);
31.9.1 Why are they called “instance private fields” in this book and
That is done to highlight how different properties (public slots) and private
slots are: By changing the order of the adjectives, the words “public” and
“field” and the words “private” and “field” are always mentioned together.
31.9.2 Why the identifier prefix #? Why not declare private fields via
private?
Could private fields be declared via private and use normal identifiers? Let’s
class MyClass {
private value; // (A)
compare(other) {
return this.value === other.value;
}
}
Is .value a property?
private field.
With option (1), we can’t use .value as a property, anymore – for any
object.
That’s why the name prefix # was introduced. The decision is now easy: If
property.
they know at compile time if other is an instance of MyClass and can then treat
.value as private or public.
VII Collections
[ES6]
32 Synchronous iteration
[ES2024]
32.5 Grouping iterables
Data sources: On one hand, data comes in all shapes and sizes. In
more.
Examples include the for-of loop and spreading into function calls (via
...).
The iteration protocol connects these two groups via the interface Iterable:
data sources deliver their contents sequentially “through it”; data consumers
Iterable Maps
spreading Strings
Figure 32.1: Data consumers such as the for-of loop use the interface
Figure 32.1 illustrates how iteration works: data consumers use the interface
sources of data.
Two roles (described by interfaces) form the core of iteration (figure 32.2):
Iterable: Iterator:
traversable data structure pointer for traversing iterable
Figure 32.2: Iteration has two main interfaces: Iterable and Iterator.
These are type definitions (in TypeScript’s notation) for the interfaces of the
iteration protocol:
interface Iterable<T> {
[Symbol.iterator]() : Iterator<T>;
}
interface Iterator<T> {
next() : IteratorResult<T>;
}
interface IteratorResult<T> {
value: T;
done: boolean;
}
Symbol.iterator.
The Iterator returns the iterated values via its method .next().
The values are not returned directly, but wrapped in objects with two
properties:
.done indicates if the end of the iteration has been reached yet. It is
The following code demonstrates how to use a while loop to iterate over an
iterable:
function logAll(iterable) {
const iterator = iterable[Symbol.iterator]();
while (true) {
const {value, done} = iterator.next();
if (done) break;
console.log(value);
}
}
logAll(['a', 'b']);
Output:
a
b
exercises/sync-iteration/sync_iteration_manually_exrc.mjs
We have seen how to use the iteration protocol manually, and it is relatively
JavaScript’s Arrays are iterable. That enables us to use the for-of loop:
a
b
c
Destructuring via Array patterns (explained later) also uses iteration under
the hood:
Output:
a
b
c
As does Array-destructuring:
const [first, second] = mySet;
assert.equal(first, 'a');
assert.equal(second, 'b');
[ES2024]
32.5 Grouping iterables
Map.groupBy() groups the items of an iterable into Map entries whose keys are
provided by a callback:
assert.deepEqual(
Map.groupBy([0, -5, 3, -4, 8, 9], x =>
Math.sign(x)),
new Map().set(0, [0]).set(-1, [-5,-4]).set(1,
[3,8,9])
);
function* generateNumbers() {
yield 2;
yield -7;
yield 4;
}
assert.deepEqual(
Map.groupBy(generateNumbers(), x => Math.sign(x)),
new Map().set(1, [2,4]).set(-1, [-7])
);
Then you need a Map. Objects can only have strings and symbols
as keys.
this section)?
following one:
const settled = [
{ status: 'rejected', reason: 'Jhon' },
{ status: 'fulfilled', value: 'Jane' },
{ status: 'fulfilled', value: 'John' },
{ status: 'rejected', reason: 'Jaen' },
{ status: 'rejected', reason: 'Jnoh' },
];
const persons = [
{ name: 'Louise', country: 'France' },
{ name: 'Felix', country: 'Germany' },
{ name: 'Ava', country: 'USA' },
{ name: 'Léo', country: 'France' },
{ name: 'Oliver', country: 'USA' },
{ name: 'Leni', country: 'Germany' },
];
assert.deepEqual(
Map.groupBy(persons, (person) => person.country),
new Map([
[
'France',
[
{ name: 'Louise', country: 'France' },
{ name: 'Léo', country: 'France' },
]
],
[
'Germany',
[
{ name: 'Felix', country: 'Germany' },
{ name: 'Leni', country: 'Germany' },
]
],
[
'USA',
[
{ name: 'Ava', country: 'USA' },
{ name: 'Oliver', country: 'USA' },
]
],
])
);
For this use case, Map.groupBy() is a better choice because we can use
arbitrary keys in Maps whereas in objects, keys are limited to strings and
symbols.
Arrays
Strings
Maps
Sets
(Browsers: DOM data structures)
func(...iterable);
const arr = [...iterable];
yield*:
function* generatorFunction() {
yield* iterable;
}
const obj =
Object.fromEntries(iterableOverKeyValuePairs);
Array.from():
const promise1 =
Promise.all(iterableOverPromises);
const promise2 =
Promise.race(iterableOverPromises);
const promise3 =
Promise.any(iterableOverPromises);
const promise4 =
Promise.allSettled(iterableOverPromises);
32.7.2.3 Grouping an iterable into a Map or an object
“Map.groupBy(items, computeGroupKey)”
“Object.groupBy(items, computeGroupKey)”
33 Arrays (Array)
Array.from()
33.7.5 Typed Arrays work well if the elements are all integers or
all floats
destructive
end of an Array
.flatMap()
element
[ES2019]
more output elements
33.17.2 Array.*
elements
sequences of elements
JavaScript Arrays are a very flexible data structure and used as lists, stacks,
destructively produce new Arrays with the changes applied to a copy of the
original content.
// Creating an Array
const arr = ['a', 'b', 'c']; // Array literal
assert.deepEqual(
arr,
[ // Array literal
'a',
'b',
'c', // trailing commas are ignored
]
);
// Reading elements
assert.equal(
arr[0], 'a' // negative indices don’t work
);
assert.equal(
arr.at(-1), 'c' // negative indices work
);
// Writing an element
arr[0] = 'x';
assert.deepEqual(
arr, ['x', 'b', 'c']
);
Output:
a
b
c
Looping over index-value pairs:
Output:
0 a
1 b
2 c
> ['■','●','▲'].join('-')
'■-●-▲'
> ['■','●','▲'].join('')
'■●▲'
.sort() sorts its receiver and returns it (if we don’t want to change the
Normally, an Array is used in either of these ways. But we can also mix the
two approaches.
As an example of the difference between the two ways, consider the Array
returned by Object.entries():
Note that the difference between fixed layout and sequence may not
Sequence Arrays are very flexible that we can use them as (traditional)
The Array literal starts and ends with square brackets []. It creates an Array
at zero):
The range of Array indices is 32 bits (excluding the maximum length): [0,
32
2 −1)
Every Array has a property .length that can be used to both read and
method .push():
> arr.push('d');
> arr
[ 'a', 'b', 'c', 'd' ]
> arr.length = 1;
> arr
[ 'a' ]
exercises/arrays/remove_empty_lines_push_test.mjs
following two invocations of .slice() are equivalent: They both copy arr
The Array method .at() returns the element at a given index. It supports
positive and negative indices (-1 refers to the last element, -2 refers to the
In contrast, the bracket operator [] does not support negative indices (and
can’t be changed because that would break existing code). It interprets them
assert.equal(
arr[-1], 'non-element property'
);
or we can assign a new empty Array to the variable storing the Array:
The latter approach has the advantage of not affecting other locations that
point to the same Array. If, however, we do want to reset a shared Array for
iterated over. Each iterated value becomes an additional Array element – for
example:
That means that we can use spreading to create a copy of an Array and to
However, for both previous use cases, I find Array.from() more self-
assert.deepEqual(
Array.from(original.keys()), [0, 1, 2]
);
Spreading is also convenient for concatenating Arrays (and other iterables)
into Arrays:
new entries in a new Array, but the values are shared with the original
assert.deepEqual(
Object.keys(arr),
['0', '1', 'prop']);
However, instanceof has one downside: It doesn’t work if a value comes from
Some realms are isolated from each other (e.g., Web Workers in browsers),
but there are also realms between which we can move data – for example,
> typeof []
'object'
We have already encountered the for-of loop earlier in this book. This
Output:
a
b
Output:
0
1
The following for-of loop iterates over [index, element] pairs. Destructuring
(described later), gives us convenient syntax for setting up index and element
Output:
0 a
1 b
Some operations that work with Arrays require only the bare minimum:
following properties:
[0]: holds the element at index 0 (etc.). Note that if we use numbers as
Arrays:
assert.deepEqual(
Array.from({length: 2, 0: 'a', 1: 'b'}),
[ 'a', 'b' ]
);
interface ArrayLike<T> {
length: number;
[n: number]: T;
}
There are two common ways of converting iterables and Array-like values
to Arrays:
Array.from()
Array.from()
to an Array:
.from<T, U>(
iterable: Iterable<T> | ArrayLike<T>,
mapFunc: (v: T, i: number) => U,
thisArg?: any)
: Array<U>
It calls mapFunc with each iterated value. The optional parameter thisArg
This is an example:
The best way of creating an Array is via an Array literal. However, we can’t
always use one: The Array may be too large, we may not know its length
Arrays.
The most common technique for creating an Array and adding elements
later, is to start with an empty Array and push values into it:
The following code creates an Array that is filled with a primitive value:
.fill() replaces each Array element or hole with a given value. We use it to
Note that the result has three holes (empty slots) – the last comma in an
If we use .fill() with an object, then each Array element will refer to this
Array-like),
In contrast to .fill(), which reuses the same object multiple times, the
Could we have used .map() in this case? Unfortunately not because .map()
elements):
For large sizes, the temporary Array in the first argument can consume quite
a bit of memory. The following approach doesn’t have this downside but is
less self-descriptive:
This works because .keys() treats holes like undefined elements and lists their
indices.
33.7.5 Typed Arrays work well if the elements are all integers or all
floats
function initMultiArray(...dimensions) {
function initMultiArrayRec(dimIndex) {
if (dimIndex >= dimensions.length) {
return 0;
} else {
const dim = dimensions[dimIndex];
const arr = [];
for (let i=0; i<dim; i++) {
arr.push(initMultiArrayRec(dimIndex+1));
}
return arr;
}
}
return initMultiArrayRec(0);
}
You’d think that Array elements are special because we are accessing them
via numbers. But the square brackets operator [] for doing so is the same
B and C):
This is only how the language specification defines things (the theory
Most JavaScript engines optimize under the hood and do use actual
will).
Property keys (strings!) that are used for Array elements are called indices.
When listing property keys, indices are treated specially – they always come
first and are sorted like numbers ('2' comes before '10'):
assert.deepEqual(
Object.keys(arr),
['0', '1', 'prop']);
Note that .length, .entries() and .keys() treat Array indices as numbers and
assert.equal(arr.length, 2);
assert.deepEqual(
Array.from(arr.keys()), [0, 1]);
assert.deepEqual(
Array.from(arr.entries()), [[0, 'a'], [1, 'b']]);
.entries() to Arrays.
An Array arr is dense if all indices i, with 0 ≤ i < arr.length, exist. That
An Array is sparse if the range of indices has holes in it. That is, some
So far, we have only seen dense Arrays and it’s indeed recommended
to avoid holes: They make our code more complicated and are not
handled consistently by Array methods. Additionally, JavaScript
Alas, there are many different ways in which Array operations treat holes.
have keys):
> Array.from(['a',,'b'].keys())
[ 0, 1, 2 ]
> Object.keys(['a',,'b'])
[ '0', '2' ]
Some Array operations are destructive: They change the Array they operate
Other Array operations are non-destructive: They produce new Arrays that
contain the desired changes and don’t touch the originals – e.g., method
.reverse()
.sort()
.splice()
We’ll get to .sort() and .splice() later in this chapter. .reverse() rearranges
an Array so that the order of its elements is reversed: The element that was
previously last now comes first; the second-last element comes second; etc.:
.toReversed(): Array
.toSorted(compareFn): Array
Array
an Array
stack, and queue. Let’s explore ways of destructively adding and removing
Array elements.
to push an Array.
.pop() is the inverse of .push() and removes elements at the end of an Array:
an Array:
const arr1 = ['a', 'b', 'c'];
assert.equal(arr1.shift(), 'a');
assert.deepEqual(arr1, ['b', 'c']);
methods:
first time we shift, we get the element at index 0; then the element
at index 1; etc.
The remaining two methods, pop and unshift, are inverses of these two
methods.
exercises/arrays/queue_via_array_test.mjs
Spread elements (...) are a feature of Array literals. In this section, we’ll
Non-destructive appending:
The following Array methods accept callbacks to which they feed Array
elements:
Finding:
.find
.findLast
.findIndex
.findLastIndex
Transforming:
.map
.flatMap
.filter
.every
.some
.reduce
.reduceRight
.forEach
That is, the callback gets three parameters (it is free to ignore any of them):
value is the most important one. This parameter holds the Array
index can additionally tell the callback what the index of the element is.
array points to the current Array (the receiver of the method call).
.map() fills its result with the values returned by its callback:
.find() returns the first Array element for which its callback returns
true:
In this section, we explore methods that accept element callbacks which tell
33.13.1 .map(): Each output element is derived from its input element
Each element of the output Array is the result of applying the callback to the
exercises/arrays/number_lines_test.mjs
For example:
exercises/arrays/remove_empty_lines_filter_test.mjs
[ES2019]
output elements
The type signature of Array<T>.prototype.flatMap() is:
.flatMap<U>(
callback: (value: T, index: number, array:
Array<T>) => U|Array<U>,
thisValue?: any
): Array<U>
output elements. That is, callback returns an Array of values (it can also
We’ll consider use cases next, before exploring how this method could be
implemented.
Array it is invoked on. That is, its callback can’t skip Array elements it isn’t
function throwIfNegative(value) {
if (value < 0) {
throw new Error('Illegal value: '+value);
}
return value;
}
We can now use .flatMap() to extract just the values or just the errors from
results:
33.13.3.2 Use case: mapping single input values to multiple output values
The Array method .map() maps each input Array element to one output
simpler than the built-in version, which, for example, performs more checks.
Exercises: .flatMap()
exercises/arrays/convert_to_numbers_test.mjs
exercises/arrays/replace_objects_test.mjs
33.14 .reduce(): computing a summary for an Array
An Array. For example, a copy of arr, where each element is twice the
original element.
Etc.
popular there. One caveat is that it can make code difficult to understand.
.reduce<U>(
callback: (accumulator: U, element: T, index:
number, array: Array<T>) => U,
init?: U)
: U
T is the type of the Array elements, U is the type of the summary. The two
may or may not be different. accumulator is just another name for “summary”.
To compute the summary of an Array arr, .reduce() feeds all Array elements
parameter accumulator) with the current Array element and returns the next
accumulator. The result of .reduce() is the final accumulator – the last result of
In other words: callback does most of the work; .reduce() just invokes it in a
useful manner.
We could say that the callback folds Array elements into the accumulator.
function addAll(arr) {
const startSum = 0;
const callback = (sum, element) => sum + element;
return arr.reduce(callback, startSum);
}
assert.equal(addAll([1, 2, 3]), 6); // (A)
assert.equal(addAll([7, -4, 2]), 5);
In this case, the accumulator holds the sum of all Array elements that
invocations of callback:
callback(0, 1) --> 1
callback(1, 2) --> 3
callback(3, 3) --> 6
Notes:
function addAll(arr) {
let sum = 0;
for (const element of arr) {
sum = sum + element;
}
return sum;
}
It’s hard to say which of the two implementations is “better”: the one based
on .reduce() is a little more concise, while the one based on for-of may be a
functional programming.
Example: finding indices via .reduce()
It returns the first index at which the given searchValue appears inside the
Array arr:
multiplied by 2:
function double(inArr) {
return inArr.reduce(
(outArr, element) => {
outArr.push(element * 2);
return outArr;
},
[]);
}
assert.deepEqual(
double([1, 2, 3]),
[2, 4, 6]);
function double(inArr) {
return inArr.reduce(
// Don’t change `outArr`, return a fresh Array
(outArr, element) => [...outArr, element * 2],
[]);
}
assert.deepEqual(
double([1, 2, 3]),
[2, 4, 6]);
This version is more elegant but also slower and uses more memory.
Exercises: .reduce()
exercises/arrays/count_matches_via_reduce_test.mjs
.reduceRight() has the same functionality but visits elements from end to
start:
representations are compared via <. This operator compares code unit values
.sort() is stable
elements are considered equal by sorting, then sorting does not change
We can customize the sort order via the parameter compareFunc, which must
zero if a is equal to b
function compareNumbers(a, b) {
if (a < b) {
return -1; // any negative number will do
} else if (a === b) {
return 0;
} else {
return 1; // any positive number will do
}
}
assert.deepEqual(
[200, 3, 10].sort(compareNumbers),
[3, 10, 200]
);
It would have to examine all Array elements and make sure that they
sorting.
The following trick uses the fact that (e.g.) the result for “less than” can be
letters, which come before all accented letters. We can use Intl, the
languages:
exercises/arrays/sort_objects_test.mjs
Arrays are iterable and therefore can use operations that accept iterables.
Legend:
such indices are added to .length before they are used: -1 becomes
second-last element, etc. .at() is one method that supports negative indices:
33.17.2 Array.*
[ES6]
Array.from(iterableOrArrayLike, mapFunc?)
Array.from<T>(
iterableOrArrayLike: Iterable<T> | ArrayLike<T>
): Array<T>
Array.from<T, U>(
iterableOrArrayLike: Iterable<T> |
ArrayLike<T>,
mapFunc: (v: T, k: number) => U, thisArg?: any
): Array<U>
Examples:
Array.of<T>(
...items: Array<T>
): Array<T>
assert.equal(
MyArray.of('a', 'b') instanceof MyArray, true
);
[R, ES2022]
Array.prototype.at(index)
returns undefined.
brackets:
[R, ES5]
Array.prototype.forEach(callback)
Array<T>.prototype.forEach(
callback: (value: T, index: number, array:
Array<T>) => void,
thisArg?: any
): void
a 0
b 1
A for-of loop is usually a better choice: it’s faster, supports break and
[R, ES6]
Array.prototype.keys()
[R, ES6]
Array.prototype.values()
[W, ES3]
Array.prototype.pop()
Removes and returns the last element of the receiver. That is, it treats
[W, ES3]
Array.prototype.push(...items)
Adds zero or more items to the end of the receiver. That is, it treats the
end of the receiver as a stack. The return value is the length of the
Removes and returns the first element of the receiver. The inverse of
.unshift().
[W, ES3]
Array.prototype.unshift(...items)
Inserts the items at the beginning of the receiver and returns its length
sequences of elements
[R, ES3]
Array.prototype.concat(...items)
Returns a new Array that is the concatenation of the receiver and all
At index start,
If deleteCount is missing, all elements from start until the end are
deleted.
[W, ES6]
Array.prototype.fill(start=0, end=this.length)
Returns this.
Assigns value to every index between (including) start and
(excluding) end.
Caveat: Don’t use this method to fill an Array with an object obj; then
each element will refer to the same value (sharing it). In this case, it’s
[W, ES6]
Array.prototype.copyWithin(target, start, end=this.length)
Returns this.
correctly.
[R, ES2016]
Array.prototype.includes(searchElement, fromIndex)
[R, ES5]
Array.prototype.lastIndexOf(searchElement, fromIndex)
Array<T>.prototype.find(
predicate: (value: T, index: number, obj:
Array<T>) => boolean,
thisArg?: any
): T | undefined
Returns the value of the first element for which predicate returns a
truthy value.
Array<T>.prototype.findLast(
predicate: (value: T, index: number, obj:
Array<T>) => boolean,
thisArg?: any
): T | undefined
truthy value.
[R, ES6]
Array.prototype.findIndex(predicate, thisArg?)
Array<T>.prototype.findIndex(
predicate: (value: T, index: number, obj:
Array<T>) => boolean,
thisArg?: any
): number
Returns the index of the first element for which predicate returns a
truthy value.
Returns the index of the first element for which predicate returns a
truthy value.
[R, ES5]
Array.prototype.filter(predicate, thisArg?)
Array<T>.prototype.filter(
predicate: (value: T, index: number, array:
Array<T>) => boolean,
thisArg?: any
): Array<T>
Returns an Array with only those elements for which predicate returns a
truthy value.
> [1, -2, 3].filter(x => x > 0)
[ 1, 3 ]
[R, ES5]
Array.prototype.map(callback, thisArg?)
Array<T>.prototype.map<U>(
mapFunc: (value: T, index: number, array:
Array<T>) => U,
thisArg?: any
): Array<U>
Array<T>.prototype.flatMap<U>(
callback: (value: T, index: number, array:
Array<T>) => U|Array<U>,
thisValue?: any
): Array<U>
“Flattens” an Array: It descends into the Arrays that are nested inside
the input Array and creates a copy where all values it finds at level
[R, ES5]
Array.prototype.every(predicate, thisArg?)
Array<T>.prototype.every(
predicate: (value: T, index: number, array:
Array<T>) => boolean,
thisArg?: any
): boolean
[R, ES5]
Array.prototype.some(predicate, thisArg?)
Array<T>.prototype.some(
predicate: (value: T, index: number, array:
Array<T>) => boolean,
thisArg?: any
): boolean
Returns true if predicate returns a truthy value for at least one element.
[R, ES5]
Array.prototype.reduce(callback, initialValue?)
Array<T>.prototype.reduce<U>(
callback: (accumulator: U, element: T, index:
number, array: Array<T>) => U,
initialValue?: U
): U
accumulator) with the current Array element and returns the next
accumulator:
Array elements.
the element at index 1 is visited first. Therefore, the Array must have at
least length 1.
[R, ES5]
Array.prototype.reduceRight(callback, initialValue?)
Array<T>.prototype.reduceRight<U>(
callback: (accumulator: U, element: T, index:
number, array: Array<T>) => U,
initialValue?: U
): U
Works like .reduce(), but visits the Array elements backward, starting
[R, ES1]
Array.prototype.toString()
Array<T>.prototype.sort(
compareFunc?: (a: T, b: T) => number
): this
Sorting numbers:
Sorting strings: By default, strings are sorted by code unit values (char
codes), where, e.g., all unaccented uppercase letters come before all
Array<T>.prototype.toSorted.toSorted(
compareFunc?: (a: T, b: T) => number
): Array<T>
See the description of .sort() for more information on how to use this
method.
[W, ES1]
Array.prototype.reverse()
Rearranges the elements of the receiver so that they are in reverse order
[R, ES2023]
Array.prototype.toReversed()
[ES6]
(advanced)
34.4.2 Endianness
[ES2024]
34.6 Resizing ArrayBuffers
[ES2024]
34.7 Transferring and detaching ArrayBuffers
34.7.1 Preparation: transferring data and detaching
34.7.6 ArrayBuffer.prototype.transferToFixedLength()
34.9.2 ArrayBuffer.*
34.10.1 TypedArray.*
34.10.2 TypedArray.prototype.*
34.10.4 «ElementType»Array.*
34.10.5 «ElementType»Array.prototype.*
34.11.2 DataView.prototype.*
Much data on the web is text: JSON files, HTML files, CSS files, JavaScript
code, etc. JavaScript handles such data well via its built-in strings.
However, before 2011, it did not handle binary data well. The Typed Array
Specification 1.0 was introduced on February 8, 2011 and provides tools for
working with binary data. With ECMAScript 6, Typed Arrays were added to
the core language and gained methods that were previously only available for
Interacting with native APIs: Native APIs often receive and return data in
a binary format, which we could neither store nor manipulate well in pre-
such an API, data had to be converted from JavaScript to binary and back
for every call. Typed Arrays eliminate this bottleneck. Examples include:
on the web for client and server applications.” For example, the
memory of WebAssembly code is stored in an ArrayBuffer or a
SharedArrayBuffer (details).
wrap it in another object – a view object. Two kinds of view objects are
available:
DataViews: let us interpret the data as various types (Uint8, Int16, Float32,
[ES2017]
34.1.3 SharedArrayBuffer
multiple agents (an agent being the main thread or a web worker) concurrently.
However, that only clones their outer parts. The data storage itself is
shared.
SharedArrayBuffers can be resized but they can only grow not shrink
cells as well as functions that let agents wait for and dispatch primitive
events. When used with discipline, the Atomics functions allow multi-
See MDN Web Docs for more information on SharedArrayBuffer and Atomics.
The following code shows three different ways of creating the same Typed
Array:
assert.deepEqual(ta1, ta2);
assert.deepEqual(ta1, ta3);
assert.deepEqual(
typedArray.buffer, new ArrayBuffer(4)); // 4 bytes
Typed Arrays don’t have a method .concat(), like normal Arrays do. The
It copies the existing typedArray or arrayLike into the receiver, at index offset.
The following function uses that method to copy zero or more Typed Arrays (or
Typed Arrays are much like normal Arrays: they have a .length, elements can
be accessed via the bracket operator [], and they have most of the standard
Array methods. They differ from normal Arrays in the following ways:
Typed Arrays have buffers. The elements of a Typed Array ta are not
new Array(4) creates a normal Array without any elements. It only has
four holes (indices less than the .length that have no associated
elements).
new Uint8Array(4) creates a Typed Array whose four elements are all 0.
assert.deepEqual(new Uint8Array(4),
Uint8Array.of(0, 0, 0, 0));
ta[0] = 257;
assert.equal(ta[0], 1); // 257 % 256 (overflow)
ta[0] = '2';
assert.equal(ta[0], 2);
The .length of a Typed Array is derived from its ArrayBuffer and never
integer
unsigned int
unsigned int
conv.)
signed int
unsigned int
signed int
Element Typed Array Bytes Description Get/Set
unsigned int
signed int
unsigned int
floating
point
floating
point
Table 34.1 lists the available element types. These types (e.g., Int32) show up in
two locations:
In Typed Arrays, they specify the types of the elements. For example, all
elements of a Int32Array have the type Int32. The element type is the only
In DataViews, they are the lenses through which they access their
The element type Uint8C is special: it is not supported by DataView and only exists
difference between Uint8C and Uint8 is how overflow and underflow are handled
Typed Arrays and Array Buffers use numbers and bigints to import and export
values:
The types BigInt64 and BigUint64 are handled via bigints. For example,
Normally, when a value is out of the range of the element type, modulo
arithmetic is used to convert it to a value within range. For signed and unsigned
The highest value plus one is converted to the lowest value (0 for unsigned
integers).
34.4.2 Endianness
endianness matters:
Big endian: the most significant byte comes first. For example, the Uint16
Little endian: the least significant byte comes first. For example, the Uint16
Endianness tends to be fixed per CPU architecture and consistent across native
APIs. Typed Arrays are used to communicate with those APIs, which is why
their endianness follows the endianness of the platform and can’t be changed.
On the other hand, the endianness of protocols and binary files varies, but is
fixed per format, across platforms. Therefore, we must be able to access data
with either endianness. DataViews serve this use case and let us specify
IPv4, IPv6, TCP, and UDP, are transmitted in big-endian order. For this
Corporation.
Other orderings are also possible. Those are generically called middle-endian
or mixed-endian.
.from<S>(
source: Iterable<S>|ArrayLike<S>,
mapfn?: S => ElementType, thisArg?: any)
: «ElementType»Array
For example, normal Arrays are iterable and can be converted with this
method:
assert.deepEqual(
Uint16Array.from([0, 1, 2]),
Uint16Array.of(0, 1, 2));
assert.deepEqual(
Uint16Array.from(Uint8Array.of(0, 1, 2)),
Uint16Array.of(0, 1, 2));
assert.deepEqual(
Uint16Array.from({0:0, 1:1, 2:2, length: 3}),
Uint16Array.of(0, 1, 2));
The optional mapfn lets us transform the elements of source before they become
elements of the result. Why perform the two steps mapping and conversion in
one go? Compared to mapping separately via .map(), there are two advantages:
2. When converting between Typed Arrays with different precisions, less can
go wrong.
The static method .from() can optionally both map and convert between Typed
assert.deepEqual(
Int16Array.from(typedArray).map(x => x * 2),
Int16Array.of(254, 252, 250)); // OK
assert.deepEqual(
Int16Array.from(typedArray.map(x => x * 2)),
Int16Array.of(-2, -4, -6)); // wrong
via .from() produces the correct result. Otherwise, we must first map and then
convert.
assert.deepEqual(
Int8Array.from(Int16Array.of(254, 252, 250), x => x
/ 2),
Int8Array.of(127, 126, 125));
assert.deepEqual(
Int8Array.from(Int16Array.of(254, 252, 250).map(x =>
x / 2)),
Int8Array.of(127, 126, 125)); // OK
assert.deepEqual(
Int8Array.from(Int16Array.of(254, 252, 250)).map(x
=> x / 2),
Int8Array.of(-1, -2, -3)); // wrong
The problem is that if we map via .map(), then input type and output type are
the same. In contrast, .from() goes from an arbitrary input type to an output
Typed Arrays are iterable. That means that we can use the for-of loop and other
iteration-based mechanisms:
Output:
0
1
2
values.
For example:
assert.deepEqual(
[...Uint8Array.of(0, 1, 2)], [0, 1, 2]
);
assert.deepEqual(
Array.from(Uint8Array.of(0, 1, 2)), [0, 1, 2]
);
[ES2024]
34.6 Resizing ArrayBuffers
Before ArrayBuffers became resizable, they had fixed sizes. If we wanted one
to grow or shrink, we had to allocate a new one and copy the old one over. That
costs time and can fragment the address space on 32-bit systems.
ArrayBuffer.prototype.resize(newByteLength: number)
get ArrayBuffer.prototype.resizable
get ArrayBuffer.prototype.maxByteLength
is resizable:
new «TypedArray»(
buffer: ArrayBuffer | SharedArrayBuffer,
byteOffset?: number,
length?: number
)
If length is undefined then the .length and .byteLength of the Typed Array instance
assert.equal(
tarr1.length, 2
);
assert.equal(
tarr2.length, 0
);
buf.resize(4);
assert.equal(
tarr1.length, 4
);
assert.equal(
tarr2.length, 2
);
bounds: The wrapper’s range isn’t covered by the ArrayBuffer anymore. That is
The ECMAScript specification gives the following guidelines for working with
resizable ArrayBuffers:
web browser could run out of memory on a 32-bit mobile web browser.
30
1,073,741,824 (2 bytes or 1 GiB).
particular maximum size does not guarantee that future resizes will
succeed.
[ES2024]
34.7 Transferring and detaching ArrayBuffers
The web API (not the ECMAScript standard) has long supported structured
cloning for safely moving values across realms (globalThis, iframes, web
workers, etc.). Some objects can also be transferred: After cloning, the original
the clone. Transfering is usually faster than copying, especially if large amounts
of memory are involved. These are the most common classes of transferable
objects:
ArrayBuffer
Streams:
ReadableStream
TransformStream
WritableStream
DOM-related data:
ImageBitmap
OffscreenCanvas
Miscellaneous communication:
MessagePort
RTCDataChannel
ArrayBuffer.prototype.transferToFixedLength(newLength?: number)
get ArrayBuffer.prototype.detached
detach) ArrayBuffers:
assert.equal(
original.byteLength, 0
);
assert.equal(
clone.byteLength, 16
);
assert.equal(
original.detached, true
);
assert.equal(
clone.detached, false
);
The ArrayBuffer method .transfer() simply gives us a more concise way to
detach an ArrayBuffer:
assert.equal(
original.detached, true
);
assert.equal(
transferred.detached, false
);
Transferring is most often used between two agents (an agent being the main
thread or a web worker). However, transferring within the same agent can make
it can transfer it so that no external code can interfere with what it does.
async function
validateAndWriteSafeAndFast(arrayBuffer) {
const owned = arrayBuffer.transfer();
Preparation:
> typedArray.length
0
> typedArray.byteLength
0
> typedArray.byteOffset
0
> typedArray.at(0)
TypeError: Cannot perform %TypedArray%.prototype.at on
a detached ArrayBuffer
34.7.6 ArrayBuffer.prototype.transferToFixedLength()
This method rounds out the API: It transfers and converts a resizable
ArrayBuffer to one with a fixed length. That may free up memory that was held
Indices for the bracket operator [ ]: We can only use non-negative indices
(starting at 0).
Every index can be negative. If it is, it is added to the length of the entity
the second-last, etc. Methods of normal Arrays work the same way.
ArrayBuffers store binary data, which is meant to be accessed via Typed Arrays
and DataViews.
34.9.1 new ArrayBuffer()
[ES6]
new ArrayBuffer(byteLength, options?)
new ArrayBuffer(
byteLength: number,
options?: { // ES2024
maxByteLength?: number
}
)
34.9.2 ArrayBuffer.*
[ES6]
ArrayBuffer.isView(arg)
or a DataView).
[ES6]
ArrayBuffer.prototype.slice(startIndex=0, endIndex=this.byteLength)
whose indices are greater than or equal to startIndex and less than endIndex.
start and endIndex can be negative (see “Quick references: indices vs.
offsets” (§34.8)).
[ES2024]
ArrayBuffer.prototype.resize(newByteLength)
Changes the size of this ArrayBuffer. For more information, see “Resizing
ArrayBuffers” (§34.6).
[ES2024]
get ArrayBuffer.prototype.resizable
[ES2024]
get ArrayBuffer.prototype.maxByteLength
The properties of the various Typed Array objects are introduced in two steps:
classes (which was shown in the class diagram at the beginning of this
chapter). That superclass is called TypedArray but it does not have a global
name in JavaScript:
> Object.getPrototypeOf(Uint8Array).name
'TypedArray'
Int16Array, Float32Array, etc. These are the classes that we use via new, .of,
and .from().
34.10.1 TypedArray.*
Both static TypedArray methods are inherited by its subclasses (Uint8Array, etc.).
Therefore, we can use these methods via the subclasses, which are concrete and
[ES6]
TypedArray.from(iterableOrArrayLike, mapFunc?)
The optional mapFunc lets us transform the elements of source before they
assert.deepEqual(
Int16Array.from(Int8Array.of(127, 126, 125), x =>
x * 2),
Int16Array.of(254, 252, 250));
[ES6]
TypedArray.of(...items)
Creates a new instance of the Typed Array class whose elements are items
assert.deepEqual(
Int16Array.of(-1234, 5, 67),
new Int16Array([-1234, 5, 67]) );
34.10.2 TypedArray.prototype.*
Indices accepted by Typed Array methods can be negative (they work like
traditional Array methods that way). Offsets must be non-negative. For details,
The following properties are specific to Typed Arrays; normal Arrays don’t
have them:
get TypedArray.prototype.buffer
get TypedArray.prototype.length
get TypedArray.prototype.byteLength
get TypedArray.prototype.byteOffset
Returns the offset where this Typed Array “starts” inside its ArrayBuffer.
TypedArray.prototype.set(typedArrayOrArrayLike, offset=0)
Copies all elements of the first parameter to this Typed Array. The
Returns a new Typed Array that has the same buffer as this Typed Array,
appropriately.
The following methods are basically the same as the methods of normal Arrays
(the ECMAScript versions specify when the methods were added to Arrays –
[R, ES2022]
TypedArray.prototype.at(index)
[W, ES6]
TypedArray.prototype.copyWithin(target, start, end=this.length)
[R, ES6]
TypedArray.prototype.entries()
[R, ES5]
TypedArray.prototype.every(predicate, thisArg?)
[W, ES6]
TypedArray.prototype.fill(start=0, end=this.length)
[R, ES5]
TypedArray.prototype.filter(predicate, thisArg?)
[R, ES6]
TypedArray.prototype.find(predicate, thisArg?)
[R, ES6]
TypedArray.prototype.findIndex(predicate, thisArg?)
[R, ES2023]
TypedArray.prototype.findLast(predicate, thisArg?)
[R, ES2023]
TypedArray.prototype.findLastIndex(predicate, thisArg?)
[R, ES5]
TypedArray.prototype.forEach(callback)
[R, ES2016]
TypedArray.prototype.includes(searchElement, fromIndex)
[R, ES5]
TypedArray.prototype.indexOf(searchElement, fromIndex)
[R, ES1]
TypedArray.prototype.join(separator = ',')
[R, ES6]
TypedArray.prototype.keys()
[R, ES5]
TypedArray.prototype.lastIndexOf(searchElement, fromIndex)
[R, ES5]
TypedArray.prototype.map(callback, thisArg?)
[R, ES5]
TypedArray.prototype.reduce(callback, initialValue?)
[R, ES5]
TypedArray.prototype.reduceRight(callback, initialValue?)
[W, ES1]
TypedArray.prototype.reverse()
[R, ES3]
TypedArray.prototype.slice(start?, end?)
[R, ES5]
TypedArray.prototype.some(predicate, thisArg?)
[W, ES1]
TypedArray.prototype.sort(compareFunc?)
[R, ES3]
TypedArray.prototype.toLocaleString()
[R, ES2023]
TypedArray.prototype.toReversed()
[R, ES2023]
TypedArray.prototype.toSorted(compareFunc?)
[R, ES2023]
TypedArray.prototype.toSpliced(start?, deleteCount?, ...items)
[R, ES1]
TypedArray.prototype.toString()
[R, ES6]
TypedArray.prototype.values()
[R, ES2023]
TypedArray.prototype.with(index, value)
For details on how these methods work, see “Quick reference: Array” (§33.17).
Each Typed Array constructor has a name that follows the pattern
Float32Array, Float64Array
BigInt64Array
BigUint64Array
Each constructor has several overloaded versions – it behaves differently
depending on how many arguments it receives and what their types are:
new «ElementType»Array(length=0)
Creates a new «ElementType»Array with the given length and the appropriate
length * «ElementType»Array.BYTES_PER_ELEMENT
(§33.5).
the buffer at the given byteOffset and will have the given length. Note that
length counts elements of the Typed Array (with 1–8 bytes each), not
bytes.
34.10.4 «ElementType»Array.*
«ElementType»Array.BYTES_PER_ELEMENT: number
> Uint8Array.BYTES_PER_ELEMENT
1
> Int16Array.BYTES_PER_ELEMENT
2
> Float64Array.BYTES_PER_ELEMENT
8
34.10.5 «ElementType»Array.prototype.*
«ElementType»Array.prototype.BYTES_PER_ELEMENT: number
[ES6]
new DataView(arrayBuffer, byteOffset?, byteLength?)
Creates a new DataView whose data is stored in the ArrayBuffer buffer.
By default, the new DataView can access all of buffer. The last two
34.11.2 DataView.prototype.*
Float32, Float64
[ES6]
get DataView.prototype.buffer
[ES6]
get DataView.prototype.byteLength
[ES6]
get DataView.prototype.byteOffset
Returns at which offset this DataView starts accessing the bytes in its
buffer.
[ES6]
DataView.prototype.get«ElementType»(byteOffset, littleEndian=false)
Returns:
[ES6]
DataView.prototype.set«ElementType»(byteOffset, value, littleEndian=false)
Type of value:
35.5.2 Map.*
object?
35.6.2 When would I use an object as a key in a Map?
Before ES6, JavaScript didn’t have a data structure for dictionaries and
values.
called an entry.
First, we can use the constructor without any parameters to create an empty
Map:
As we’ll see later, Maps are also iterables over key-value pairs. Therefore,
we can use the constructor to create a copy of a Map. That copy is shallow:
keys and values are the same; they are not duplicated.
.set() and .get() are for writing and reading values (given keys).
assert.equal(map.get('foo'), 123);
// Unknown key:
assert.equal(map.get('bar'), undefined);
// Use the default value '' if an entry is missing:
assert.equal(map.get('bar') ?? '', '');
.has() checks if a Map has an entry with a given key. .delete() removes
entries.
assert.equal(map.has('foo'), true);
assert.equal(map.delete('foo'), true)
assert.equal(map.has('foo'), false)
.size contains the number of entries in a Map. .clear() removes all entries of
a Map.
assert.equal(map.size, 2)
map.clear();
assert.equal(map.size, 0)
Output:
false
true
assert.deepEqual(
Array.from(map.keys()),
[false, true]);
Output:
[ false, 'no' ]
[ true, 'yes' ]
assert.deepEqual(
Array.from(map.entries()),
[[false, 'no'], [true, 'yes']]);
Map instances are also iterables over entries. In the following code, we use
Output:
false no
true yes
Maps record in which order entries were created and honor that order when
As long as a Map only uses strings and symbols as keys, we can convert it
We can also convert an object to a Map with string or symbol keys (via
Object.entries()):
const obj = {
a: 1,
b: 2,
};
const map = new Map(Object.entries(obj));
assert.deepEqual(
map, new Map([['a', 1], ['b', 2]]));
function countChars(chars) {
const charCounts = new Map();
for (let ch of chars) {
ch = ch.toLowerCase();
const prevCount = charCounts.get(ch) ?? 0;
charCounts.set(ch, prevCount+1);
}
return charCounts;
}
map.set(KEY1, 'hello');
map.set(KEY2, 'world');
assert.equal(map.get(KEY1), 'hello');
assert.equal(map.get(KEY2), 'world');
As a consequence, we can use NaN as a key in Maps, just like any other
value:
roadmap).
We can .map() and .filter() an Array, but there are no such operations for a
Mapping originalMap:
Filtering originalMap:
To combine map1 and map2 we create a new Array and spread (...) the entries
(key-value pairs) of map1 and map2 into it (via iteration). Then we convert the
exercises/maps/combine_maps_test.mjs
Note: For the sake of conciseness, I’m pretending that all keys have the
same type K and that all values have the same type V.
[ES6]
new Map(entries?)
35.5.2 Map.*
[ES2024]
Map.groupBy(items, computeGroupKey)
Map.groupBy<K, T>(
items: Iterable<T>,
computeGroupKey: (item: T, index: number) => K,
): Map<K, Array<T>>;
items.
its value is an Array with all items that have that group key.
assert.deepEqual(
Map.groupBy(
['orange', 'apricot', 'banana', 'apple',
'blueberry'],
(str) => str[0] // compute group key
),
new Map()
.set('o', ['orange'])
.set('a', ['apricot', 'apple'])
.set('b', ['banana', 'blueberry'])
);
35.5.3 Map.prototype.*: handling single entries
[ES6]
Map.prototype.get(key)
Returns the value that key is mapped to in this Map. If there is no key
[ES6]
Map.prototype.set(key, value)
This method returns this, which means that we can chain it.
[ES6]
Map.prototype.has(key)
[ES6]
get Map.prototype.size
[ES6]
Map.prototype.clear()
Both iterating and looping happen in the order in which entries were added
to a Map.
[ES6]
Map.prototype.entries()
Returns an iterable with one [key, value] pair for each entry in this
Output:
[ 1, 'one' ]
[ 2, 'two' ]
[ES6]
Map.prototype.forEach(callback, thisArg?)
Map.prototype.forEach(
callback: (value: V, key: K, theMap: Map<K,V>)
=> void,
thisArg?: any
): void
Output:
one 1
two 2
[ES6]
Map.prototype.keys()
Output:
1
2
[ES6]
Map.prototype.values()
Output:
one
two
[ES6]
Map.prototype[Symbol.iterator]()
Output:
1 one
2 two
35.6.1 When should I use a Map, and when should I use an object?
If we need a dictionary-like data structure with keys that are neither strings
If, however, our keys are either strings or symbols, we must decide whether
Then use an object obj and access the values via fixed keys:
Then use a Map map and access the values via keys stored in variables:
considered equal if they have the same content). That excludes objects.
However, there is one use case for objects as keys: externally attaching data
to objects. But that use case is served better by WeakMaps, where entries
don’t prevent keys from being garbage-collected (for details, consult the
next chapter).
that operations that list entries, keys, or values are deterministic. That helps,
.length, while unindexed collections (such as Maps and Sets) have a .size:
[ES2023]
36.2.2 Why are symbols as WeakMap keys interesting?
They are black boxes, where a value can only be accessed if we have
use cases:
computed results.
The next two sections examine in more detail what that means.
For example, we can’t iterate or loop over keys, values or entries. And
fresh instance.
affected by someone who has both the weakmap and the key. With
clear(), someone with only the WeakMap would’ve been able to affect
The keys of a WeakMap are said to be weakly held: Normally if one object
WeakMap still exists. That also leads to the corresponding entry being
[ES6]
Objects
[ES2023]
Symbols – as long as they are not registered (created via
Symbol.for())
All kinds of keys have one thing in common – they have identity semantics:
1. When compared via ===, two keys are considered equal if they have the
(their values). That means there are never two or more different keys
Both conditions are important so that WeakMaps can dispose entries when
primitive but they are compared by identity and they are garbage-
collected.
[ES2023]
36.2.2 Why are symbols as WeakMap keys interesting?
features:
WeakMap cache.
If we use this function with an object obj, we can see that the result is only
computed for the first invocation, while a cached value is used for the
second invocation:
In the following code, the WeakMaps _counter and _action are used to store
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
exercises/weakmaps/weakmaps_private_data_test.mjs
The constructor and the four methods of WeakMap work the same as their Map
equivalents:
[ES6]
new WeakMap<K, V>(entries?: Iterable<[K, V]>)
[ES6]
WeakMap.prototype.delete(key: K) : boolean
[ES6]
WeakMap.prototype.get(key: K) : V
[ES6]
WeakMap.prototype.has(key: K) : boolean
[ES6]
WeakMap.prototype.set(key: K, value: V) : this
[ES6]
37 Sets (Set)
37.4.1 Union (a ∪ b)
37.4.2 Intersection (a ∩ b)
37.4.3 Difference (a \ b)
Before ES6, JavaScript didn’t have a data structure for sets. Instead, two
Since ES6, JavaScript has the data structure Set, which can contain arbitrary
First, you can use the constructor without any parameters to create an empty
Set:
Second, you can pass an iterable (e.g., an Array) to the constructor. The
assert.equal(set.has('red'), true);
set.clear();
assert.equal(set.size, 0)
Sets are iterable and the for-of loop works as you’d expect:
Output:
red
green
blue
As you can see, Sets preserve insertion order. That is, elements are always
Given that Sets are iterable, you can use Array.from() to convert them to
Arrays:
Converting an Array to a Set and back, removes duplicates from the Array:
assert.deepEqual(
Array.from(new Set([1, 2, 1, 2, 3, 3, 3])),
[1, 2, 3]);
Strings are iterable and can therefore be used as parameters for new Set():
assert.deepEqual(
new Set('abc'),
new Set(['a', 'b', 'c']));
As with Map keys, Set elements are compared similarly to ===, with the
> set.add({});
> set.size
1
> set.add({});
> set.size
2
Sets are missing several common operations. Such an operation can usually
be implemented by:
37.4.1 Union (a ∪ b)
Computing the union of two Sets a and b means creating a Set that contains
37.4.2 Intersection (a ∩ b)
Computing the intersection of two Sets a and b means creating a Set that
assert.deepEqual(
Array.from(intersection), [2, 3]
);
37.4.3 Difference (a \ b)
Computing the difference between two Sets a and b means creating a Set
that contains those elements of a that are not in b. This operation is also
assert.deepEqual(
Array.from(difference), [1]
);
Sets don’t have a method .map(). But we can borrow the one that Arrays
have:
method:
const set = new Set([1, 2, 3, 4, 5]);
const filteredSet = new Set(
Array.from(set).filter(x => (x % 2) === 0)
);
assert.deepEqual(
Array.from(filteredSet), [2, 4]
);
Sets are iterable and therefore can use operations that accept iterables.
[ES6]
new Set(iterable)
created.
If you do, then the iterated values are added as elements to the
Set.
[ES6]
Set.prototype.has(value)
[ES6]
get Set.prototype.size
[ES6]
Set.prototype.values()
Output:
red
green
[ES6]
Set.prototype[Symbol.iterator]()
Output:
red
green
[ES6]
Set.prototype.forEach(callback, thisArg?)
forEach(
callback: (value: T, key: T, theSet: Set<T>) =>
void,
thisArg?: any
): void
Feeds each element of this Set to callback(). value and key both contain
Map.prototype.forEach().
You can specify the this of callback via thisArg. If you omit it, this is
undefined.
Output:
red
green
The following two methods mainly exist so that Sets and Maps have similar
interfaces. Each Set element is handled as if it were a Map entry whose key
[ES6]
Set.prototype.entries(): Iterable<[T,T]>
[ES6]
Set.prototype.keys(): Iterable<T>
The answer to this question is given in “Why do Maps have a .size, while
They can hold objects without preventing those objects from being
garbage-collected.
They are black boxes: we only get any data out of a WeakSet if we have
both the WeakSet and a value. The only methods that are supported are
Given that we can’t iterate over their elements, there are not that many use
The following code demonstrates how a class can ensure that its methods
Domenic Denicola):
method() {
if (!instancesOfSafeClass.has(this)) {
throw new TypeError('Incompatible object!');
}
}
}
assert.throws(
() => {
const obj = {};
SafeClass.prototype.method.call(obj); // throws
an exception
},
TypeError
);
equivalents:
[ES6]
new WeakSet<T>(values?: Iterable<T>)
[ES6]
.add(value: T): this
[ES6]
.delete(value: T): boolean
[ES6]
.has(value: T): boolean
[ES6]
39 Destructuring
39.4 Object-destructuring
39.5 Array-destructuring
39.9 (Advanced)
With normal assignment, you extract one piece of data at a time – for
example:
With destructuring, you can extract multiple pieces of data at the same time
via patterns in locations that receive data. The left-hand side of = in the
previous code is one such location. In the following code, the square
Note that the pattern is “smaller” than the data: we are only extracting what
we need.
You can extract data out of compound data, for example, by getting
properties.
assert.deepEqual(jane1, jane2);
const jane = {
first: 'Jane',
last: 'Doe',
};
// Extracting: one property at a time
const f1 = jane.first;
const l1 = jane.last;
assert.equal(f1, 'Jane');
assert.equal(l1, 'Doe');
Destructuring patterns are syntactically similar to the literals that are used
for multivalue construction. But they appear where data is received (e.g., at
the left-hand sides of assignments), not where data is created (e.g., at the
Variable declarations:
const [a] = ['x'];
assert.equal(a, 'x');
Assignments:
let b;
[b] = ['z'];
assert.equal(b, 'z');
Parameter definitions:
Note that variable declarations include const and let declarations in for-of
loops:
Output:
0 a
1 b
In the next two sections, we’ll look deeper into the two kinds of
39.4 Object-destructuring
const address = {
street: 'Evergreen Terrace',
number: '742',
city: 'Springfield',
state: 'NT',
zip: '49007',
};
You can think of the pattern as a transparent sheet that you place over the
data: the pattern key 'street' has a match in the data. Therefore, the data
Exercise: Object-destructuring
exercises/destructuring/object_destructuring_exrc.mjs
In object literals, you can have spread properties. In object patterns, you can
const obj = { a: 1, b: 2, c: 3 };
const { a: propValue, ...remaining } = obj; // (A)
assert.equal(propValue, 1);
assert.deepEqual(remaining, {b:2, c:3});
A rest property variable, such as remaining (line A), is assigned an object
with all data properties whose keys are not mentioned in the pattern.
syntactic ambiguity – you can’t start a statement with a curly brace because
let prop;
assert.throws(
() => eval("{prop} = { prop: 'hello' };"),
{
name: 'SyntaxError',
message: "Unexpected token '='",
});
Why eval()?
eval() delays parsing (and therefore the SyntaxError) until the callback
executed.
39.5 Array-destructuring
The first element of the Array pattern in line A is a hole, which is why the
Arrays:
In Array literals, you can have spread elements. In Array patterns, you can
A rest element variable, such as remaining (line A), is assigned an Array with
all elements of the destructured value that were not mentioned yet.
You can use Array-destructuring to swap the values of two variables without
let x = 'a';
let y = 'b';
assert.equal(x, 'b');
assert.equal(y, 'a');
assert.equal(year, '2999');
assert.equal(month, '12');
assert.equal(day, '31');
Its second parameter is a function that receives the value and index of an
element and returns a boolean indicating if this is the element the caller is
looking for.
We are now faced with a dilemma: Should findElement() return the value of
the element it found or the index? One solution would be to create two
separate functions, but that would result in duplicated code because both
As we are working with property keys, the order in which we mention value
The kicker is that destructuring also serves us well if we are only interested
anything?
What happens if there is no match for part of a pattern? The same thing that
get undefined:
get undefined:
const [x] = [];
assert.equal(x, undefined);
undefined or null. That is, it fails whenever accessing a property via the dot
Therefore, you can’t Array-destructure undefined and null. But you can’t
undefined:
If you want a different value to be used, you need to specify a default value
(via =):
In line A, we specify the default value for p to be 123. That default is used
because the data that we are destructuring has no property named prop.
Here, we have two default values that are assigned to the variables x and y
destructured.
The default value for the first element of the Array pattern is 1; the default
Neither property key first nor property key last exist in the object that is
have much in common with an Array pattern (rest elements, default values,
function f2(...args) {
const [«pattern1», «pattern2»] = args;
// ···
}
Until now, we have only used variables as assignment targets (data sinks)
inside destructuring patterns. But you can also use patterns as assignment
const arr = [
{ first: 'Jane', last: 'Bond' },
{ first: 'Lars', last: 'Croft' },
];
const [, {first}] = arr; // (A)
assert.equal(first, 'Lars');
Inside the Array pattern in line A, there is a nested object pattern at index 1.
Nested patterns can become difficult to understand, so they are best used in
moderation.
[ES6]
40 Synchronous generators
(advanced)
yield
40.1.1 Generator functions return iterables and fill them via yield
If we call a generator function, it returns an iterable (actually, an iterator
that is also iterable). The generator fills that iterable via the yield operator:
function* genFunc1() {
yield 'a';
yield 'b';
}
Output:
a
b
into the body of the generator function until there is a yield that returns
a value.
Therefore, yield does more than just add values to iterables – it also pauses
Like return, a yield exits the body of the function and returns a value
(to/via .next()).
Let’s examine what that means via the following generator function.
let location = 0;
function* genFunc2() {
location = 1; yield 'a';
location = 2; yield 'b';
location = 3;
}
genFunc2() and executes it until there is a yield. Then execution pauses and
assert.deepEqual(
iter.next(), {value: 'a', done: false});
// genFunc2() is now paused directly after the first
`yield`:
assert.equal(location, 1);
Note that the yielded value 'a' is wrapped in an object, which is how
paused. Once we encounter the second yield, genFunc2() is paused and .next()
assert.deepEqual(
iter.next(), {value: 'b', done: false});
// genFunc2() is now paused directly after the
second `yield`:
assert.equal(location, 2);
We call iter.next() one more time and execution continues until it leaves the
body of genFunc2():
assert.deepEqual(
iter.next(), {value: undefined, done: true});
// We have reached the end of genFunc2():
assert.equal(location, 3);
This time, property .done of the result of .next() is true, which means that the
iterator is finished.
What are the benefits of yield pausing execution? Why doesn’t it simply
work like the Array method .push() and fill the iterable with values without
pausing?
processes that are multitasked cooperatively). For example, when we ask for
the next value of an iterable, that value is computed lazily (on demand). The
/**
* Returns an iterable over lines
*/
function* genLines() {
yield 'A line';
yield 'Another line';
yield 'Last line';
}
/**
* Input: iterable over lines
* Output: iterable over numbered lines
*/
function* numberLines(lineIterable) {
let lineNumber = 1;
for (const line of lineIterable) { // input
yield lineNumber + ': ' + line; // output
lineNumber++;
}
}
Note that the yield in numberLines() appears inside a for-of loop. yield can be
used inside loops, but not inside callbacks (more on that later).
numbered line. In turn, it asks genLines() for only a single unnumbered line.
This incrementalism continues to work if, for example, genLines() reads its
lines from a large text file: If we ask numberLines() for a numbered line, we
get one as soon as genLines() has read its first line from the text file.
Without generators, genLines() would first read all lines and return them.
Then numberLines() would number all lines and return them. We therefore
have to wait much longer until we get the first numbered line.
exercises/sync-generators/fib_seq_test.mjs
The following function mapIter() is similar to the Array method .map(), but it
exercises/sync-generators/filter_iter_gen_test.mjs
yield only works directly inside generators – so far we haven’t seen a way of
Let’s first examine what does not work: in the following example, we’d like
foo() to call bar(), so that the latter yields two values for the former. Alas, a
function* bar() {
yield 'a';
yield 'b';
}
function* foo() {
// Nothing happens if we call `bar()`:
bar();
}
assert.deepEqual(
Array.from(foo()), []
);
Why doesn’t this work? The function call bar() returns an iterable, which we
ignore.
What we want is for foo() to yield everything that is yielded by bar(). That’s
function* bar() {
yield 'a';
yield 'b';
}
function* foo() {
yield* bar();
}
assert.deepEqual(
Array.from(foo()), ['a', 'b']
);
function* foo() {
for (const x of bar()) {
yield x;
}
}
function* gen() {
yield* [1, 2];
}
assert.deepEqual(
Array.from(gen()), [1, 2]
);
iterating over recursive data structures such as trees. Take, for example, the
class BinaryTree {
constructor(value, left=null, right=null) {
this.value = value;
this.left = left;
this.right = right;
}
means that we can use a for-of loop to iterate over an instance of BinaryTree:
Output:
a
b
c
d
e
exercises/sync-generators/iter_nested_arrays_test.mjs
40.3 Background: external iteration vs. internal iteration
In preparation for the next section, we need to learn about two different
External iteration (pull): Your code asks the object for the values via an
iteration protocol:
Output:
a
b
the object and the method feeds the values to the callback. For
Output:
a
b
The next section has examples for both styles of iteration.
One important use case for generators is extracting and reusing traversals.
and logs their paths (it uses the Node.js API for doing so):
function logPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.join(dir, fileName);
console.log(filePath);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
logPaths(filePath); // recursive call
}
}
}
mydir/
a.txt
b.txt
subdir/
c.txt
Let’s log the paths inside mydir/:
logPaths('mydir');
Output:
mydir/a.txt
mydir/b.txt
mydir/subdir
mydir/subdir/c.txt
How can we reuse this traversal and do something other than logging the
paths?
One way of reusing traversal code is via internal iteration: Each traversed
function* iterPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.join(dir, fileName);
yield filePath; // (A)
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
yield* iterPaths(filePath);
}
}
}
const paths = Array.from(iterPaths('mydir'));
40.5 Advanced features of generators
The chapter on generators in Exploring ES6 covers two features that are
Generators can also return values (not just yield them). Such values do
programming in JavaScript
41.7 Resources
JavaScript.
41.1 A roadmap for asynchronous programming in
JavaScript
programming in JavaScript.
Normal functions are synchronous: the caller waits until the callee is
function call:
function main() {
try {
const result = divideSync(12, 3); // (A)
assert.equal(result, 4);
} catch (err) {
assert.fail(err);
}
}
while (true) {
const task = taskQueue.dequeue();
task(); // run task
}
This loop is also called the event loop because events, such as clicking a
other tasks from being executed while, for example, it waits for results
coming from a server. The next subsection explores how to handle this case.
What if divide() needs a server to compute its result? Then the result should
the caller.
function main() {
divideCallback(12, 3,
(err, result) => {
if (err) {
assert.fail(err);
} else {
assert.equal(result, 4);
}
});
}
divideCallback(x, y, callback)
Then the current task main() is finished and other tasks can be executed.
function main() {
dividePromise(12, 3)
.then(result => assert.equal(result, 4))
.catch(err => assert.fail(err));
}
code:
syntax for handling the call. await can only be used inside a special kind of
function, an async function (note the keyword async in front of the keyword
function). await pauses the current async function and returns from it. Once
the awaited result is ready, the execution of the function continues where it
left off.
In this chapter, we’ll see how synchronous function calls work. We’ll
return to after the latter function is finished. That is typically done via a
stack – the call stack: the caller pushes onto it the location to return to, and
function h(z) {
const error = new Error();
console.log(error.stack);
}
function g(y) {
h(y + 1);
}
function f(x) {
g(x + 1);
}
f(3);
Initially, before running this piece of code, the call stack is empty. After the
function call f(3) in line 11, the stack has one entry:
After the function call g(x + 1) in line 9, the stack has two entries:
After the function call h(y + 1) in line 6, the stack has three entries:
Error
at h (demos/async-js/stack_trace.mjs:2:17)
at g (demos/async-js/stack_trace.mjs:7:3)
at f (demos/async-js/stack_trace.mjs:10:3)
at demos/async-js/stack_trace.mjs:12:1
This is a so-called stack trace of where the Error object was created. Note
that it records where calls were made, not return locations. Creating the
exception in line 2 is yet another call. That’s why the stack trace includes a
After line 3, each of the functions terminates and each time, the top entry is
removed from the call stack. After function f is done, we are back in top-
level scope and the stack is empty. When the code fragment ends then that is
executed, then returning with an empty call stack ends the task.
code) inside that process. The event loop is depicted in figure 41.1.
Event loop ↺
Task queue
Figure 41.1: Task sources add code to run to the task queue, which is
Task sources add tasks to the queue. Some of those sources run
The event loop runs continuously inside the JavaScript process. During
each loop iteration, it takes one task out of the queue (if the queue is
empty, it waits until it isn’t) and executes it. That task is finished when
the call stack is empty and there is a return. Control goes back to the
event loop, which then retrieves the next task from the queue and
while (true) {
const task = taskQueue.dequeue();
task(); // run task
}
block the user interface. Let’s look at a web page that demonstrates that.
There are two ways in which you can try out that page:
You can open the following file inside the repository with the
exercises: demos/async-js/blocking.html
The idea is that you click “Block” and a long-running loop is executed via
JavaScript. During that loop, you can’t click the button because the
document.getElementById('block')
.addEventListener('click', doBlock); // (A)
function doBlock(event) {
// ···
displayStatus('Blocking...');
// ···
sleep(5000); // (B)
displayStatus('Done');
}
function sleep(milliseconds) {
const start = Date.now();
while ((Date.now() - start) < milliseconds);
}
function displayStatus(status) {
document.getElementById('statusMessage')
.textContent = status;
}
doBlock() displays status information and then calls sleep() to block the
sleep() blocks the JavaScript process by looping until enough time has
passed.
statusMessage.
There are several ways in which you can prevent a long-running operation
finished. The invocation is handled via the task queue. This style of
wait until the results are ready. Normal function calls deliver their
results synchronously.
run concurrently to the main process. Each one of them has its own
how.
The following global function executes its parameter callback after a delay of
features):
The function returns a handle (an ID) that can be used to clear the timeout
(cancel the execution of the callback) via the following global function:
shows it in action.
Each task is always finished (“run to completion”) before the next task
is executed.
As a consequence, tasks don’t have to worry about their data being changed
programming in JavaScript.
console.log('start');
setTimeout(() => {
console.log('callback');
}, 0);
console.log('end');
Output:
start
end
callback
setTimeout() puts its parameter into the task queue. The parameter is
completely finished.
The parameter ms only specifies when the task is put into the queue, not
when exactly it runs. It may even never run – for example, if there is a task
before it in the queue that never terminates. That explains why the previous
In order to avoid blocking the main process while waiting for a long-
Events
Callbacks
Promises
The first two patterns are explained in the next two subsections. Promises
parameter.
The event source sends events and lets you register event listeners.
of using it:
The parameters for the operation are provided via the request object,
not via parameters of the method. For example, the event listeners
The invocation of the operation is added to the task queue via the
method (in line A). That is, we configure the operation after its
that the operation runs after the current code fragment is finished.
The XMLHttpRequest API lets us make downloads from within a web browser.
function processData(str) {
assert.equal(str, 'Content of textfile.txt\n');
}
With this API, we first create a request object (line A), then configure it,
Specifying which HTTP request method to use (line B): GET, POST, PUT,
etc.
that some of the result data is delivered via the request object xhr. (I’m
error.
We have already seen DOM events in action in “The user interface of the
browser can be blocked” (§41.4.1). The following code also handles click
events:
const element = document.getElementById('my-link');
// (A)
element.addEventListener('click', clickListener); //
(B)
function clickListener(event) {
event.preventDefault(); // (C)
console.log(event.shiftKey); // (D)
}
We first ask the browser to retrieve the HTML element whose ID is 'my-link'
(line A). Then we add a listener for all click events (line B). In the listener,
we first tell the browser not to perform its default action (line C) – going to
the target of the link. Then we log to the console if the shift key is currently
Callbacks are another pattern for handling asynchronous results. They are
only used for one-off results and have the advantage of being less verbose
than events.
returns its contents asynchronously. This is how you call readFile() if it uses
Node.js-style callbacks:
There is a single callback that handles both success and failure. If the first
parameter is not null then an error happened. Otherwise, the result can be
The following exercises use tests for asynchronous code, which are
js/read_file_cb_exrc.mjs
js/map_cb_test.mjs
must use asynchronous code. In this chapter, we have seen several patterns
that such code can use. All of them have two disadvantages:
Asynchronous code is more verbose than synchronous code.
The first disadvantage becomes less severe with Promises (covered in the
next chapter) and mostly disappears with async functions (covered in the
Alas, the infectiousness of async code does not go away. But it is mitigated
by the fact that switching between sync and async is easy with async
functions.
41.7 Resources
[ES6]
programming
[ES2018]
42.1.9 Promise.prototype.finally()
[ES2024]
42.1.10 Promise.withResolvers()
42.2 Examples
asynchronously
Promises
42.5.1 What is a Promise combinator function?
42.5.2 Promise.all()
42.5.3 Promise.race()
[ES2021]
42.5.4 Promise.any()
[ES2020]
42.5.5 Promise.allSettled()
42.8.1 Promise.all()
42.8.2 Promise.race()
[ES2021]
42.8.3 Promise.any()
[ES2020]
42.8.4 Promise.allSettled()
Recommended reading
addAsync(3, 4)
.then(result => { // success
assert.equal(result, 7);
})
.catch(error => { // failure
assert.fail(error);
});
(if and when it is done). The Promise passes it on to the relevant callbacks.
In contrast to the event pattern, Promises are optimized for one-off results:
On one hand, it is a placeholder and container for the final result that
numbers x and y:
function addAsync(x, y) {
return new Promise(
(resolve, reject) => { // (A)
if (x === undefined || y === undefined) {
reject(new Error('Must provide two
parameters'));
} else {
resolve(x + y);
}
});
}
addAsync() immediately invokes the Promise constructor. The actual
that constructor (line A). That callback is provided with two functions:
API:
consumer, say
doThingsWith(p);
then we can be sure that this consumer cannot mess with any of the
to, for example, putting resolve and reject methods on p, which anyone
could call.
Settled
Pending Fulfilled
Rejected
called settled.
Figure 42.1 depicts the three states a Promise can be in. Promises specialize
cached. Thus, if .then() or .catch() are called after the settlement, they
Additionally, once a Promise is settled, its state and settlement value can’t
change anymore. That helps make code predictable and enforces the one-off
nature of Promises.
If x is fulfilled, p is fulfilled.
If x is rejected, p is rejected.
In other words: The operation resolve only determines the fate of a Promise;
and Promise.reject()
Promise.resolve(123)
.then(x => {
assert.equal(x, 123);
});
below.
function convertToNumber(stringOrNumber) {
if (typeof stringOrNumber === 'number') {
return Promise.resolve(stringOrNumber);
} else if (typeof stringOrNumber === 'string') {
return stringToNumberAsync(stringOrNumber);
} else {
return Promise.reject(new TypeError());
}
}
so enables method chaining: We can invoke .then() and .catch() on the result
First, the callback can return a non-Promise value (line A). Consequently,
the Promise returned by .then() is fulfilled with that value (as checked in
line B):
Promise.resolve('abc')
.then(str => {
return str + str; // (A)
})
.then(str2 => {
assert.equal(str2, 'abcabc'); // (B)
});
Second, the callback can return a Promise q (line A). Consequently, the
effectively replaced by q.
Promise.resolve('abc')
.then(str => {
return Promise.resolve(123); // (A)
})
.then(num => {
assert.equal(num, 123);
});
and process its fulfillment value via a “flat” (non-nested) .then(). Compare:
// Flat
asyncFunc1()
.then(result1 => {
/*···*/
return asyncFunc2();
})
.then(result2 => {
/*···*/
});
// Nested
asyncFunc1()
.then(result1 => {
/*···*/
asyncFunc2()
.then(result2 => {
/*···*/
});
});
rejections, not fulfillments. However, both methods turn the actions of their
callbacks into Promises in the same manner. For example, in the following
fulfillment value:
Promise.reject(err)
.catch(e => {
assert.equal(e, err);
// Something went wrong, use a default value
return 'default value'; // (A)
})
.then(str => {
assert.equal(str, 'default value');
});
function myAsyncFunc() {
return asyncFunc1() // (A)
.then(result1 => {
// ···
return asyncFunc2(); // a Promise
})
.then(result2 => {
// ···
return result2 ?? '(Empty)'; // not a Promise
})
.then(result3 => {
// ···
return asyncFunc4(); // a Promise
});
}
Due to chaining, the return in line A returns the result of the last .then().
We can also add .catch() into the mix and let it handle multiple error
asyncFunc1()
.then(result1 => {
// ···
return asyncFunction2();
})
.then(result2 => {
// ···
})
.catch(error => {
// Failure: handle errors of asyncFunc1(),
asyncFunc2()
// and any (sync) exceptions thrown in previous
callbacks
});
[ES2018]
42.1.9 Promise.prototype.finally()
somePromise
.then((result) => {
// ···
})
.catch((error) => {
// ···
})
.finally(() => {
// ···
})
;
.finally() ignores what its callback returns and simply passes on the
Promise.resolve(123)
.finally(() => {})
.then((result) => {
assert.equal(result, 123);
});
Promise.reject('error')
.finally(() => {})
.catch((error) => {
assert.equal(error, 'error');
});
Promise.reject('error (originally)')
.finally(() => {
throw 'error (finally)';
})
.catch((error) => {
assert.equal(error, 'error (finally)');
});
One common use case for .finally() is similar to a common use case of the
synchronous finally clause: cleaning up after you are done with a resource.
let connection;
db.open()
.then((conn) => {
connection = conn;
return connection.select({ name: 'Jane' });
})
.then((result) => {
// Process result
// Use `connection` to make more queries
})
// ···
.catch((error) => {
// handle errors
})
.finally(() => {
connection.close();
});
42.1.9.2 Use case for .finally(): doing something first after any kind of settlement
We can also use .finally() before both .then() and .catch(). Then what we do
in the .finally() callback is always executed before the other two callbacks.
function handleAsyncResult(promise) {
return promise
.finally(() => {
console.log('finally');
})
.then((result) => {
console.log('then ' + result);
})
.catch((error) => {
console.log('catch ' + error);
})
;
}
handleAsyncResult(Promise.resolve('fulfilled'));
Output:
finally
then fulfilled
handleAsyncResult(Promise.reject('rejected'));
Output:
finally
catch rejected
[ES2024]
42.1.10 Promise.withResolvers()
The most common way of creating and resolving a Promise is via the Promise
constructor:
new Promise(
(resolve, reject) => { ··· }
);
One limitation of creating Promises like that is that the settlement functions
resolve and reject are meant to only be used inside the callback. Sometimes
we want to use them outside of it. That’s when the following static factory
method is useful:
{
const { promise, resolve, reject } =
Promise.withResolvers();
resolve('fulfilled');
assert.equal(
await promise,
'fulfilled'
);
}
{
const { promise, resolve, reject } =
Promise.withResolvers();
reject('rejected');
try {
await promise;
} catch (err) {
assert.equal(err, 'rejected');
}
}
42.1.10.1 An implementation
function promiseWithResolvers() {
let resolve;
let reject;
const promise = new Promise(
(res, rej) => {
// Executed synchronously!
resolve = res;
reject = rej;
});
return {promise, resolve, reject};
}
class OneElementQueue {
#promise = null;
#resolve = null;
constructor() {
const { promise, resolve } =
Promise.withResolvers();
this.#promise = promise;
this.#resolve = resolve;
}
get() {
return this.#promise;
}
put(value) {
this.#resolve(value);
}
}
These are some of the advantages of Promises over plain callbacks when it
input, while the one or two callbacks at the end are about output. With
synchronous errors: Inside the callbacks for new Promise(), .then(), and
One of the biggest advantages of Promises involves not working with them
42.2 Examples
examples.
Consider the following text file person.json with JSON data in it:
{
"first": "Jane",
"last": "Doe"
}
Let’s look at two versions of code that reads this file and parses it into an
The following code reads the contents of this file and converts it to a
line C, we convert that string from the text-based data format JSON into a
Note that there are two error-handling mechanisms: the if in line A takes
version of fs.readFile():
callback via method .then() of that Promise. The remaining code in then’s
callback is synchronous.
Note that .catch() lets us handle both the asynchronous errors of readFile()
function httpGet(url) {
return new Promise(
(resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.responseText); // (A)
} else {
// Something went wrong (404, etc.)
reject(new Error(xhr.statusText)); // (B)
}
}
xhr.onerror = () => {
reject(new Error('Network error')); // (C)
};
xhr.open('GET', url);
xhr.send();
});
}
Note how the results and errors of XMLHttpRequest are handled via resolve()
and reject():
httpGet('http://example.com/textfile.txt')
.then(content => {
assert.equal(content, 'Content of
textfile.txt\n');
})
.catch(error => {
assert.fail(error);
});
Exercise: Timing out a Promise
exercises/promises/promise_timeout_test.mjs
interface Body {
text() : Promise<string>;
···
}
interface Response extends Body {
···
}
declare function fetch(str) : Promise<Response>;
fetch('http://example.com/textfile.txt')
.then(response => response.text())
.then(text => {
assert.equal(text, 'Content of textfile.txt\n');
});
Exercise: Using the fetch API
exercises/promises/fetch_json_test.mjs
This makes our synchronous and asynchronous code more predictable and
For Promise-based functions and methods, the rule means that they should
never throw exceptions. Alas, it is easy to accidentally get this wrong – for
example:
// Don’t do this
function asyncFunc() {
doSomethingSync(); // (A)
return doSomethingAsync()
.then(result => {
// ···
});
}
throw an exception. Callers of that function only expect rejections and are
not prepared for an exception. There are three ways in which we can fix this
issue.
We can wrap the whole body of the function in a try-catch statement and
// Solution 1
function asyncFunc() {
try {
doSomethingSync();
return doSomethingAsync()
.then(result => {
// ···
});
} catch (err) {
return Promise.reject(err);
}
}
Promise.
// Solution 2
function asyncFunc() {
return Promise.resolve()
.then(() => {
doSomethingSync();
return doSomethingAsync();
})
.then(result => {
// ···
});
}
// Solution 3
function asyncFunc() {
return new Promise((resolve, reject) => {
doSomethingSync();
resolve(doSomethingAsync());
})
.then(result => {
// ···
});
}
asynchronously
Their execution starts right away, synchronously (in the current task).
function asyncFunc() {
console.log('asyncFunc');
return new Promise(
(resolve, _reject) => {
console.log('new Promise()');
resolve();
});
}
console.log('START');
asyncFunc()
.then(() => {
console.log('.then()'); // (A)
});
console.log('END');
Output:
START
asyncFunc
new Promise()
END
.then()
We can see that the callback of new Promise() is executed before the end of
example in the next chapter, where text is written to a file and race
Promises
42.5.2 Promise.all()
Promise.all<T>(promises: Iterable<Promise<T>>):
Promise<Array<T>>
of promises.
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.resolve('result c'),
];
Promise.all(promises)
.then((arr) => assert.deepEqual(
arr, ['result a', 'result b', 'result c']
));
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.reject('ERROR'),
];
Promise.all(promises)
.catch((err) => assert.equal(
err, 'ERROR'
));
all
∀ ∃
✓ ✗
[v 0
, v 1
, ···] r i
Array transformation methods such as .map(), .filter(), etc., are made for
function timesTwoSync(x) {
return 2 * x;
}
const arr = [1, 2, 3];
const result = arr.map(timesTwoSync);
assert.deepEqual(result, [2, 4, 6]);
function that maps normal values to Promises)? Then the result of .map() is
an Array of Promises. Alas, that is not data that normal code can work with.
Next, we’ll use .map() and Promise.all() to downlooad text files from the web.
function downloadText(url) {
return fetch(url)
.then((response) => { // (A)
if (!response.ok) { // (B)
throw new Error(response.statusText);
}
return response.text(); // (C)
});
}
string:
const urls = [
'http://example.com/first.txt',
'http://example.com/second.txt',
];
Promise.all(promises)
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));
safety checks):
function all(iterable) {
return new Promise((resolve, reject) => {
let elementCount = 0;
let result;
let index = 0;
for (const promise of iterable) {
// Preserve the current value of `index`
const currentIndex = index;
promise.then(
(value) => {
result[currentIndex] = value;
elementCount++;
if (elementCount === result.length) {
resolve(result); // (A)
}
},
(err) => {
reject(err); // (B)
});
index++;
}
if (index === 0) {
resolve([]);
return;
}
// Now we know how many Promises there are in
`iterable`.
// We can wait until now with initializing
`result` because
// the callbacks of .then() are executed
asynchronously.
result = new Array(index);
});
}
The two main locations where the result Promise is settled are line A and
line B. After one of them settled, the other can’t change the settlement value
42.5.3 Promise.race()
Promise.race<T>(promises: Iterable<Promise<T>>):
Promise<T>
happens before the settlement of the rejected Promise (line B). Therefore,
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 100)), //
(A)
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 200)), // (B)
];
Promise.race(promises)
.then((result) => assert.equal( // (C)
result, 'result'));
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 200)),
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 100)),
];
Promise.race(promises)
.then(
(result) => assert.fail(),
(err) => assert.equal(
err, 'ERROR'));
Note that the Promise returned by Promise.race() is settled as soon as the first
among its input Promises is settled. That means that the result of
race
st st
1 1
✓ ✗
v i
r i
/**
* Returns a Promise that is resolved with `value`
* after `ms` milliseconds.
*/
function resolveAfter(ms, value=undefined) {
return new Promise((resolve, _reject) => {
setTimeout(() => resolve(value), ms);
});
}
/**
* Returns a Promise that is rejected with `reason`
* after `ms` milliseconds.
*/
function rejectAfter(ms, reason=undefined) {
return new Promise((_resolve, reject) => {
setTimeout(() => reject(reason), ms);
});
}
To produce the second Promise, timeout() uses the fact that resolving a
pending Promise with a rejected Promise leads to the former being rejected.
Let’s see timeout() in action. Here, the input Promise is fulfilled before the
Here, the timeout happens before the input Promise is fulfilled. Therefore,
That is, timing out only prevents the input Promise from affecting the output
(since a Promise can only be settled once). But it does not stop the
safety checks):
function race(iterable) {
return new Promise((resolve, reject) => {
for (const promise of iterable) {
promise.then(
(value) => {
resolve(value); // (A)
},
(err) => {
reject(err); // (B)
});
}
});
}
The result Promise is settled in either line A or line B. Once it is, the
[ES2021]
42.5.4 Promise.any()
Promise.any<T>(promises: Iterable<Promise<T>>):
Promise<T>
Promise.
any
∃ ∀
✓ ✗
v [r 0
, r 1
, ···]
i
constructor(
errors: Iterable<any>,
message: string = '',
options?: ErrorOptions // ES2022
);
}
interface ErrorOptions {
cause?: any; // ES2022
}
const promises = [
Promise.reject('ERROR A'),
Promise.reject('ERROR B'),
Promise.resolve('result'),
];
Promise.any(promises)
.then((result) => assert.equal(
result, 'result'
));
const promises = [
Promise.reject('ERROR A'),
Promise.reject('ERROR B'),
Promise.reject('ERROR C'),
];
Promise.any(promises)
.catch((aggregateError) => assert.deepEqual(
aggregateError.errors,
['ERROR A', 'ERROR B', 'ERROR C']
));
42.5.4.3 Promise.any() vs. Promise.all()
compared:
an error object).
things:
use cases for .any() are broader. We’ll look at them next.
are only interested in the first successful one. In a way, we let the
computations compete with each other and use whichever one is fastest.
The following code demonstrates what that looks like when downloading
resources:
quickly:
For comparison, this is the code we’d use if the secondary server is only a
implementation of Promise.all().
[ES2020]
42.5.5 Promise.allSettled()
This time, the type signatures are a little more complicated. Feel free to skip
Promise.allSettled<T>(promises:
Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>
It returns a Promise for an Array whose elements have the following type
signature:
interface RejectionObject {
status: 'rejected';
reason: unknown;
}
Promise.allSettled() returns a Promise out. Once all promises are settled, out is
Promise p of promises:
Unless there is an error when iterating over promises, the output Promise out
is never rejected.
x = { x = {
i allSettled i
value: v i
reason: r i
}
✓ ✗ }
iteration
[x 0
, x 1
, ···]
error
Promise.allSettled([
Promise.resolve('a'),
Promise.reject('b'),
])
.then(arr => assert.deepEqual(arr, [
{ status: 'fulfilled', value: 'a' },
{ status: 'rejected', reason: 'b' },
]));
The next example is similar to the .map() plus Promise.all() example (from
const urls = [
'http://example.com/exists.txt',
'http://example.com/missing.txt',
];
no safety checks):
function allSettled(iterable) {
return new Promise((resolve, reject) => {
let elementCount = 0;
let result;
let index = 0;
for (const promise of iterable) {
// Capture the current value of `index`
const currentIndex = index;
promise.then(
(value) => addElementToResult(
currentIndex, {
status: 'fulfilled',
value
}),
(reason) => addElementToResult(
currentIndex, {
status: 'rejected',
reason
}));
index++;
}
if (index === 0) {
resolve([]);
return;
}
// Now we know how many Promises there are in
`iterable`.
// We can wait until now with initializing
`result` because
// the callbacks of .then() are executed
asynchronously.
result = new Array(index);
});
}
settled early – before all input Promises are settled. The following
combinators short-circuit:
Promise is rejected.
Promise is settled.
Promise is fulfilled.
Once again, settling early does not mean that the operations behind the
ignored Promises are stopped. It just means that their settlements are
ignored.
asyncFunc1()
.then(result1 => {
assert.equal(result1, 'one');
return asyncFunc2();
})
.then(result2 => {
assert.equal(result2, 'two');
});
Promise.all([asyncFunc1(), asyncFunc2()])
.then(arr => {
assert.deepEqual(arr, ['one', 'two']);
});
when asynchronous operations start, not on how their Promises are handled.
asyncFunc2() concurrently because they are started at nearly the same time.
function concurrentAll() {
return Promise.all([asyncFunc1(), asyncFunc2()]);
}
function concurrentThen() {
const p1 = asyncFunc1();
const p2 = asyncFunc2();
return p1.then(r1 => p2.then(r2 => [r1, r2]));
}
On the other hand, both of the following functions execute asyncFunc1() and
asyncFunc1() is fulfilled.
function sequentialThen() {
return asyncFunc1()
.then(r1 => asyncFunc2()
.then(r2 => [r1, r2]));
}
function sequentialAll() {
const p1 = asyncFunc1();
const p2 = p1.then(() => asyncFunc2());
return Promise.all([p1, p2]);
}
Promise.all([
// (A) fork
downloadText('http://example.com/first.txt'),
downloadText('http://example.com/second.txt'),
])
// (B) join
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));
Problem:
// Don’t do this
function foo() {
const promise = asyncFunc();
promise.then(result => {
// ···
});
return promise;
}
Computation starts with the Promise returned by asyncFunc(). But afterward,
returns the former Promise, but should return the latter. This is how to fix it:
function foo() {
const promise = asyncFunc();
return promise.then(result => {
// ···
});
}
Problem:
// Don’t do this
asyncFunc1()
.then(result1 => {
return asyncFunc2()
.then(result2 => { // (A)
// ···
});
});
asyncFunc1()
.then(result1 => {
return asyncFunc2();
})
.then(result2 => {
// ···
});
// Don’t do this
asyncFunc1()
.then(result1 => {
if (result1 < 0) {
return asyncFuncA()
.then(resultA => 'Result: ' + resultA);
} else {
return asyncFuncB()
.then(resultB => 'Result: ' + resultB);
}
});
asyncFunc1()
.then(result1 => {
return result1 < 0 ? asyncFuncA() :
asyncFuncB();
})
.then(resultAB => {
return 'Result: ' + resultAB;
});
db.open()
.then(connection => { // (A)
return connection.select({ name: 'Jane' })
.then(result => { // (B)
// Process result
// Use `connection` to make more queries
})
// ···
.finally(() => {
connection.close(); // (C)
});
})
so that we have access to variable connection inside the callback and in line
C.
Problem:
// Don’t do this
class Model {
insertInto(db) {
return new Promise((resolve, reject) => { // (A)
db.insert(this.fields)
.then(resultCode => {
this.notifyObservers({event: 'created',
model: this});
resolve(resultCode);
}).catch(err => {
reject(err);
})
});
}
// ···
}
class Model {
insertInto(db) {
return db.insert(this.fields)
.then(resultCode => {
this.notifyObservers({event: 'created',
model: this});
return resultCode;
});
}
// ···
}
The key idea is that we don’t need to create a Promise; we can return the
result of the .then() call. An additional benefit is that we don’t need to catch
and re-reject the failure of db.insert(). We simply pass its rejection on to the
caller of .insertInto().
Glossary:
42.8.1 Promise.all()
Promise.all<T>(promises: Iterable<Promise<T>>)
: Promise<Array<T>>
Short-circuits: yes
Use case: processing Arrays with Promises (rejections terminate
processing)
42.8.2 Promise.race()
Promise.race<T>(promises: Iterable<Promise<T>>)
: Promise<T>
Short-circuits: yes
[ES2021]
42.8.3 Promise.any()
Promise.any<T>(promises: Iterable<Promise<T>>):
Promise<T>
Promises.
Short-circuits: yes
interested in the first successful one. That is, we are trying several
[ES2020]
42.8.4 Promise.allSettled()
Promise.allSettled<T>(promises:
Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>
Value: Array with one settlement object for each input Promise. A
settlement value.
Promises.
Short-circuits: no
processing)
interface FulfillmentObject<T> {
status: 'fulfilled';
value: T;
}
interface RejectionObject {
status: 'rejected';
reason: unknown;
}
[ES2017]
43 Async functions
[ES2022]
43.3.4 Using await at the top levels of modules
43.4 (Advanced)
Roughly, async functions provide better syntax for code that uses Promises.
function fetchJsonViaPromises(url) {
return fetch(url) // async
.then(request => request.text()) // async
.then(text => JSON.parse(text)) // sync
.catch(error => {
assert.fail(error);
});
}
fetchJsonAsync('http://example.com/person.json')
.then(obj => {
assert.deepEqual(obj, {
first: 'Jane',
last: 'Doe',
});
});
Promises directly
From the outside, it is virtually impossible to tell the difference
Promise-based function.
confusing).
Inside the async function, we fulfill the result Promise via return (line A):
asyncFunc()
.then(result => {
assert.equal(result, 123);
});
asyncFunc()
.then(result => {
assert.equal(result, undefined);
});
asyncFunc()
.catch(err => {
assert.deepEqual(err, new Error('Problem!'));
});
of the function (or rather, the result “locks in” on p and behaves exactly like
it). That is, the Promise is not wrapped in yet another Promise.
settlement (advanced)
The Promise resultPromise for the result is created when the async
function is started.
Then the body is executed. There are two ways in which execution can
is a Promise p:
settled.
temporary) exit.
Note that the notification of the settlement of resultPromise happens
synchronously (line A), then the current task finishes (line C), then the
Output:
asyncFunc() starts
Task ends
Resolved: abc
The await operator can only be used inside async functions and async
performed:
The current async function is paused and returned from. This step is
Eventually, the current task is finished and processing of the task queue
continues.
new task:
Read on to find out more about how await handles Promises in various
states.
If its operand ends up being a fulfilled Promise, await returns its fulfillment
value:
If its operand is a rejected Promise, then await throws the rejection value:
try {
await Promise.reject(new Error());
assert.fail(); // we never get here
} catch (e) {
assert.equal(e instanceof Error, true);
}
exercises/async-functions/fetch_json2_test.mjs
If we are inside an async function and want to pause it via await, we must do
so directly within that function; we can’t use it inside a nested function, such
The reason is that normal arrow functions don’t allow await inside their
bodies.
Alas, this doesn’t work either: Now .map() (and therefore downloadContent())
Promise via await, only to re-wrap it immediately via return. If we omit await,
let mylib;
try {
mylib = await
import('https://primary.example.com/mylib');
} catch {
mylib = await
import('https://secondary.example.com/mylib');
}
(§29.15).
exercises/async-functions/map_async_test.mjs
43.4 (Advanced)
In the next two subsections, we’ll use the helper function paused():
/**
* Resolves after `ms` milliseconds
*/
function delay(ms) {
return new Promise((resolve, _reject) => {
setTimeout(resolve, ms);
});
}
async function paused(id) {
console.log('START ' + id);
await delay(10); // pause
console.log('END ' + id);
return id;
}
START first
END first
START second
END second
finished.
method Promise.all():
Output:
START first
START second
END first
END second
Here, both asynchronous functions are started at the same time. Once both
previous one:
Output:
START first
START second
END first
END second
need it if we want to pause and wait until the returned Promise is settled. If
conditions.
It can occasionally make sense to use await, even if we ignore its result – for
example:
await longRunningAsyncOperation();
console.log('Done!');
That ensures that the logging really happens after that operation is done.
[ES2018]
44 Asynchronous iteration
generator
Required knowledge
Promises
Async functions
44.1 Basic asynchronous iteration
interface Iterable<T> {
[Symbol.iterator]() : Iterator<T>;
}
interface Iterator<T> {
next() : IteratorResult<T>;
}
interface IteratorResult<T> {
value: T;
done: boolean;
}
Each IterationResult contains the iterated .value and a boolean .done that
For the protocol for asynchronous iteration, we only want to change one
In other words, the question is whether to wrap just values or whole iterator
results in Promises.
value or signals the end of the iteration can only be determined after it is
interface AsyncIterable<T> {
[Symbol.asyncIterator]() : AsyncIterator<T>;
}
interface AsyncIterator<T> {
next() : Promise<IteratorResult<T>>; // (A)
}
interface IteratorResult<T> {
value: T;
done: boolean;
}
We call .next() in line B, line C and line D. Each time, we use .then() to
Promises via await and the code looks almost like we are doing synchronous
iteration:
and async generators (which are introduced later in this chapter). This is an
Output:
a
b
Output:
a
b
Promises:
const arr = [Promise.resolve('a'),
Promise.resolve('b')];
for await (const x of arr) {
console.log(x);
}
Output:
a
b
Warning: We’ll soon see the solution for this exercise in this chapter.
exercises/async-iteration/async_iterable_to_array_test.mjs
data.
generators
Due to async generators and sync generators being so similar, I don’t
// Output
yield someValue;
yield* otherAsyncGen();
}
three numbers:
async iterable to an Array (think spreading, but for async iterables instead
of sync iterables).
Note that we can’t use an async generator in this case: We get our input via
Note the await in line A, which is needed to unwrap the Promise returned by
Warning: We’ll soon see the solution for this exercise in this chapter.
exercises/async-iteration/number_lines_test.mjs
Note how similar the sync implementation and the async implementation
are. The only two differences are the async in line A and the await in line B.
occasional await.
Once again, we await to unwrap a Promise (line A) and this code fragment
Exercise: filterAsyncIter()
exercises/async-iteration/filter_async_iter_test.mjs
callbacks:
function main(inputFilePath) {
const readStream =
fs.createReadStream(inputFilePath,
{ encoding: 'utf-8', highWaterMark: 1024 });
readStream.on('data', (chunk) => {
console.log('>>> '+chunk);
});
readStream.on('end', () => {
console.log('### DONE ###');
});
}
That is, the stream is in control and pushes data to the reader.
Starting with Node.js 10, we can also use asynchronous iteration to read
from streams:
This time, the reader is in control and pulls data from the stream.
Node.js streams iterate over chunks (arbitrarily long pieces) of data. The
/**
* @param chunkIterable An asynchronous or
synchronous iterable
* over “chunks” (arbitrary strings)
* @returns An asynchronous iterable over “lines”
* (strings with at most one newline that always
appears at the end)
*/
async function* chunksToLines(chunkIterable) {
let previous = '';
for await (const chunk of chunkIterable) {
let startSearch = previous.length;
previous += chunk;
while (true) {
// Works for EOL === '\n' and EOL === '\r\n'
const eolIndex = previous.indexOf('\n',
startSearch);
if (eolIndex < 0) break;
// Line includes the EOL
const line = previous.slice(0, eolIndex+1);
yield line;
previous = previous.slice(eolIndex+1);
startSearch = 0;
}
}
if (previous.length > 0) {
yield previous;
}
}
chunkIterable()):
Now that we have an asynchronous iterable over lines, we can use the
expressions
\S \w \W
[ES2018]
45.4.2 Unicode character property escapes
[ES2024]
45.4.3 Unicode string property escapes
[ES2024]
45.5.1 String literals in character classes
[ES2024]
45.5.2 Set operations for character classes
[ES2018]
45.8.2 Lookbehind assertions
[ES6]
45.10.3 Flag /u: matching code points
[ES2024]
clusters
[ES2022]
45.12.1 Match indices in match objects
string
[ES3]
45.13.2 regExp.test(str): is there a match?
[ES3]
45.13.3 str.search(regExp): at what index is the match?
[ES3]
45.13.4 regExp.exec(str): capturing groups
[ES3]
45.13.5 str.match(regExp): getting all group 0 captures
[ES2020]
objects
45.14 The flags /g and /y, and the property .lastIndex (advanced)
index
45.14.5 The downsides of .lastIndex
Availability of features
/abc/iv
The flags i and v. Flags configure how the pattern is interpreted. For
[ES3]
new RegExp(pattern : string, flags = '')
[ES6]
new RegExp(regExp : RegExp, flags = regExp.flags)
clone.
while modifying them. Flags are immutable and this is the only way of
At the top level of a regular expression, the following syntax characters are
\ ^ $ . * + ? ( ) [ ] { } |
> /\//.test('/')
true
Without flag /u and /v, an escaped non-syntax character at the top level
matches itself:
> /^\a$/.test('a')
true
syntax error:
assert.throws(
() => eval(String.raw`/\a/v`),
{
name: 'SyntaxError',
message: 'Invalid regular expression: /\\a/v:
Invalid escape',
}
);
assert.throws(
() => eval(String.raw`/\-/v`),
{
name: 'SyntaxError',
message: 'Invalid regular expression: /\\-/v:
Invalid escape',
}
);
( ) [ ] { } / - \ |
. matches any character. We can use the flag /s (dotAll) to control if the
\w \W
[ES2018]
Unicode character property escapes define sets of code
(§45.4.2).
[ES2024]
Unicode string property escapes define sets of code point
(§45.4.3).
Without /u and /v, a character is a UTF-16 code unit. With those flags, a
\W
The following character class escapes and their complements are always
supported:
Digits \d [0-9] \D
Whitespace \s \S
Note:
terminators, etc. They all fit into single UTF-16 code units.
Examples:
> 'a7x4'.match(/\d/g)
[ '7', '4' ]
> 'a7x4'.match(/\D/g)
[ 'a', 'x' ]
> 'high - low'.match(/\w+/g)
[ 'high', 'low' ]
> 'hello\t\n everyone'.replaceAll(/\s/g, '-')
'hello---everyone'
[ES2018]
45.4.2 Unicode character property escapes
With flag /u and flag /v, we can use \p{} and \P{} to specify sets of code
points via Unicode character properties (we’ll learn more about those in the
Comments:
\p{Uppercase_Letter}
\p{General_Category=Uppercase_Letter}
Examples:
x: General_Category = Lowercase_Letter
$: General_Category = Currency_Symbol
White_Space: used for marking invisible spacing characters, such as
π: White_Space = False
introduced – for example: The Euro sign € was added in version 2.1 of
€: Age = 2.1
Block: a contiguous range of code points. Blocks don’t overlap and their
🙂 Block = Emoticons
: (range 0x1F600..0x1F64F)
systems.
Examples:
α: Script = Greek
Д: Script = Cyrillic
Further reading:
[ES2024]
45.4.3 Unicode string property escapes
With /u, we can use Unicode property escapes (\p{} and \P{}) to specify sets
With /v, we can additionally use \p{} to specify sets of code point sequences
Let’s see how the character property Emoji would do with these inputs:
JavaScript:
Emoji_Keycap_Sequence
RGI_Emoji_Modifier_Sequence
RGI_Emoji_Flag_Sequence
RGI_Emoji_Tag_Sequence
RGI_Emoji_ZWJ_Sequence
Further reading:
are.
JavaScript.
The semantics of Unicode string properties are defined in text files that
A character class wraps class ranges in square brackets. The class ranges
Only the following four characters are special and must be escaped via
slashes:
^ \ - ]
usual meanings.
[ES2024]
45.5.1 String literals in character classes
Flag /v enables a new feature inside character classes – we can use \q{} to
😵💫
false
> /^[ ]$/v.test('\u{1F635}')
true
> /^[\q{abc|def}]$/v.test('abc')
true
> /^[\q{abc|def}]$/v.test('def')
true
[ES2024]
45.5.2 Set operations for character classes
To enable set operations for character classes, we must be able to nest them.
> /^[\d\w]$/v.test('7')
true
> /^[\d\w]$/v.test('H')
true
> /^[\d\w]$/v.test('?')
false
With flag /v, we can additionally nest character classes (the regular
example):
> /^[[0-9][A-Za-z0-9_]]$/v.test('7')
true
> /^[[0-9][A-Za-z0-9_]]$/v.test('H')
true
> /^[[0-9][A-Za-z0-9_]]$/v.test('?')
false
> /^[\w--[a-g]]$/v.test('a')
false
> /^[\w--[a-g]]$/v.test('h')
true
> /^[\p{Number}--[0-9]]$/v.test('٣')
true
> /^[\p{Number}--[0-9]]$/v.test('3')
false
> /^[\p{RGI_Emoji}--\q{ 😵💫}]$/v.test('😵💫') // emoji
has 3 code points
😵💫}]$/v.test('🙂')
false
> /^[\p{RGI_Emoji}--\q{
true
Single code points can also be used on either side of the -- operator:
> /^[\w--a]$/v.test('a')
false
> /^[\w--a]$/v.test('b')
true
We can use the && operator to set-theoretically intersect the character sets
> /[\p{ASCII}&&\p{Letter}]/v.test('D')
true
> /[\p{ASCII}&&\p{Letter}]/v.test('Δ')
false
> /^[\p{Script=Arabic}&&\p{Number}]$/v.test('٣')
true
> /^[\p{Script=Arabic}&&\p{Number}]$/v.test(')'ج
false
write their definining constructs next to each other inside a character class:
2️⃣
> /^[\p{Emoji_Keycap_Sequence}[a-
z]]+$/v.test('a c')
true
[ES2018]
Named capture group : (?<hashes>#+)
Backreference: \k<hashes>
By default, all of the following quantifiers are greedy (they match as many
characters as possible):
To make them reluctant (so that they match as few characters as possible),
Pattern Name
next.
comes next.
[ES2018]
45.8.2 Lookbehind assertions
before.
came before.
^aa|zz$ matches all strings that start with aa and/or end with zz.
Literal Property
ES Description
flag name
terminators
(recommended)
matches
JavaScript.
The following regular expression flags are available in JavaScript (table 45.1
If this flag is on, each match object includes match indices which tell
[ES2022]
“Match indices in match objects ” (§45.12.1).
RegExp.prototype.test()
RegExp.prototype.exec()
String.prototype.match()
How, is explained in “The flags /g and /y, and the property .lastIndex”
(§45.14). In a nutshell, without /g, the methods only consider the first
match for a regular expression in an input string. With /g, they consider
all matches.
> /a/.test('A')
false
> /a/i.test('A')
true
and $ matches the end of each line. If it is off, ^ matches the beginning
of the whole input string and $ matches the end of the whole input
string.
> 'a1\na2\na3'.match(/^a./gm)
[ 'a1', 'a2', 'a3' ]
> 'a1\na2\na3'.match(/^a./g)
[ 'a1' ]
/s (.dotAll): By default, the dot does not match line terminators. With
> /./.test('\n')
false
> /./s.test('\n')
true
/u (.unicode): This flag provides better support for Unicode code points
[ES6]
and is explained in “Flag /u: matching code points ” (§45.10.3).
[ES2024]
support for multi-code-point grapheme clusters ” (§45.10.4).
/y (.sticky): This flag mainly makes sense in conjunction with /g. When
both are switched on, any match must directly follow the previous one
information on this flag: “The flags /g and /y, and the property
.lastIndex” (§45.14).
Given that (2) is not obvious, (1) is the better choice. JavaScript also uses it
> /-/gymdivs.flags
'dgimsvy'
Without the flags /u and /v, most constructs work with single UTF-16 code
🙂
units – which is a problem whenever there is a code point with two code
units – such as :
🙂
> ' '.match(/./g)
[ '\uD83D', '\uDE42' ]
🙂
> /^ {2}$/.test('\uD83D\uDE42\uDE42')
true
> /^\uD83D\uDE42{2}$/.test('\uD83D\uDE42\uDE42') //
equivalent
true
🙂
> ' '.match(/\D/g)
[ '\uD83D', '\uDE42' ]
🙂
> /^[ ]$/.test(' ') 🙂
false
> /^[\uD83D\uDE42]$/.test('\uD83D\uDE42') //
equivalent
🙂
false
> /^[ ]$/.test('\uD83D')
true
[ES6]
45.10.3 Flag /u: matching code points
🙂
In the previous subsection, we encountered problems when we wanted to
match a code point with more than one UTF-16 code unit – such as .
Flag /u enables support for this kind of code point and fixes those problems.
We can use code point escapes – \u{} with one to six hexadecimal digits:
🙂'.match(/./gu)
🙂' ]
> '
[ '
> /^ 🙂{2}$/u.test('🙂🙂')
true
🙂'.match(/\D/gu)
🙂' ]
> '
[ '
property escapes:
[ES2024]
clusters
and should be used by default. If you can’t use it yet because it’s still
of its shortcomings.
Note that flag /v and flag /u are mutually exclusive – we can’t use them
assert.throws(
() => eval('/-/uv'),
SyntaxError
);
45.10.4.1 Limitation of flag /u: handling grapheme clusters with more than one code point
😵💫
Some font glyphs are represented by grapheme clusters (code point
😵💫
assert.equal(
' '.match(/./gu).length, 3
);
😵💫 😵💫😵💫
assert.equal(
/^ {2}$/u.test(' '), false
);
😵💫
assert.equal(
/^\p{Emoji}$/u.test(' '), false
);
// Character classes only match single code points
😵💫 😵💫
assert.equal(
/^[ ]$/u.test(' '), false
);
45.10.4.2 Flag /v: Unicode string property escapes and character class string literals
Flag /v works like flag /u but provides better support for multi-code-point
everywhere, but it does fix the last two issues we encountered in the
clusters to:
\p{}:
classes:
> /^[\q{ 😵💫}]$/v.test('😵💫')
true
Character classes can be nested and combined via the set operations
[ES2024]
” (§45.5.2).
> /^\P{Lowercase_Letter}$/iu.test('A')
true
> /^\P{Lowercase_Letter}$/iu.test('a')
true
> /^[^\p{Lowercase_Letter}]$/iu.test('A')
false
> /^[^\p{Lowercase_Letter}]$/iu.test('a')
false
Observations:
> /^\P{Lowercase_Letter}$/iv.test('A')
false
> /^\P{Lowercase_Letter}$/iv.test('a')
false
> /^[^\p{Lowercase_Letter}]$/iv.test('A')
false
> /^[^\p{Lowercase_Letter}]$/iv.test('a')
false
Further reading:
nested class”
Noteworthy:
descriptive name:
> /a/i.ignoreCase
true
> /a/.ignoreCase
false
.dotAll (/s)
.global (/g)
.hasIndices (/d)
.ignoreCase (/i)
.multiline (/m)
.sticky (/y)
.unicode (/u)
.unicodeSets (/v)
[ES3]
.source : The regular expression pattern
> /abc/ig.source
'abc'
[ES6]
.flags : The flags of the regular expression
> /abc/ig.flags
'gi'
[ES3]
.lastIndex : Used when flag /g is switched on. Consult “The flags /g
not set).
This is an example:
assert.deepEqual(
/(a+)b/d.exec('ab aaab'),
{
0: 'ab',
1: 'a',
index: 0,
input: 'ab aaab',
groups: undefined,
indices: {
0: [0, 2],
1: [0, 1],
groups: undefined
},
}
);
The result of .exec() is a match object for the first match with the following
properties:
[ES2018]
” (§45.13.4.2))
[ES2022]
45.12.1 Match indices in match objects
Match indices are a feature of match objects: If we turn it on via the regular
expression flag /d (property .hasIndices), they record the start and end
Due to the regular expression flag /d, matchObj also has a property .indices
that records for each numbered group where it was captured in the input
string:
assert.deepEqual(
matchObj.indices[1], [0, 4]
);
assert.deepEqual(
matchObj.indices[2], [4, 6]
);
assert.deepEqual(
matchObj.indices.groups.as, [0, 4]);
assert.deepEqual(
matchObj.indices.groups.bs, [4, 6]);
One important use case for match indices are parsers that point to where
problem: It points to where quoted content starts and where it ends (see
assert.equal(
pointToQuotedText(
'They said “hello” and “goodbye”.'),
' [ ] [ ] '
);
> /a/.test('__a__')
true
We can change that by using assertions such as ^ or by using the flag /y:
> /^a/.test('__a__')
false
> /^a/.test('a__')
true
[ES3]
45.13.2 regExp.test(str): is there a match?
The regular expression method .test() returns true if regExp matches str:
> /bc/.test('ABCD')
false
> /bc/i.test('ABCD')
true
> /\.mjs$/.test('main.mjs')
true
With .test() we should normally avoid the /g flag. If we use it, we generally
don’t get the same result every time we call the method:
The results are due to /a/ having two matches in the string. After all of those
[ES3]
45.13.3 str.search(regExp): at what index is the match?
The string method .search() returns the first index of str at which there is a
[ES3]
45.13.4 regExp.exec(str): capturing groups
Without the flag /g, .exec() returns a match object for the first match of
regExp in str:
assert.deepEqual(
/(a+)b/.exec('ab aab'),
{
0: 'ab',
1: 'a',
index: 0,
input: 'ab aab',
groups: undefined,
}
);
[ES2018]
45.13.4.2 Named capture groups
In the result of .exec(), we can see that a named group is also a numbered
fewer caveats.
If we want to retrieve all matches of a regular expression (not just the first
one), we need to switch on the flag /g. Then we can call .exec() multiple
times and get one match each time. After the last match, .exec() returns null.
let match;
// Check for null via truthiness
// Alternative: while ((match = regExp.exec(str))
!== null)
while (match = regExp.exec(str)) {
console.log(match[1]);
}
Output:
a
aa
Be careful when sharing regular expressions with /g!
explained later.
exercises/regexps/extract_quoted_test.mjs
[ES3]
45.13.5 str.match(regExp): getting all group 0 captures
Without /g, .match() works like .exec() – it returns a single match object.
With /g, .match() returns all substrings of str that match regExp:
> 'xyz'.match(/(a+)b/g)
null
We can use the nullish coalescing operator (??) to protect ourselves against
null:
const numberOfMatches = (str.match(regExp) ??
[]).length;
[ES2020]
> Array.from('-a-a-a'.matchAll(/-(a)/ug))
[
{ 0:'-a', 1:'a', index: 0, input: '-a-a-a',
groups: undefined },
{ 0:'-a', 1:'a', index: 2, input: '-a-a-a',
groups: undefined },
{ 0:'-a', 1:'a', index: 4, input: '-a-a-a',
groups: undefined },
]
> Array.from('-a-a-a'.matchAll(/-(a)/u))
TypeError: String.prototype.matchAll called with a
non-global
RegExp argument
localCopy.lastIndex is zero.
Using matchAll():
Output:
fee
fi
fo
fum
Without /g With /g
str.replace(searchValue, replacementValue)
str.replaceAll(searchValue, replacementValue)
searchValue can be:
A string
A regular expression
String: Replace matches with this string. The character $ has special
meaning and lets us insert captures of groups and more (details are
explained later).
with /g.
Search for:
string RegExp w/o /g RegExp with /g
→
expression.
expression.
expression that matches that string multiple times (e.g., '*' becomes
/\*/g).
expression.
So far, we have only used the parameter replacementValue with simple strings,
A string, then matches are replaced with this string. The character $ has
special meaning and lets us insert captures of groups and more (read
on for details).
A function, then matches are replaced by strings that are computed via
this function.
If the replacement value is a string, the dollar sign has special meaning – it
Text Result
$$ single $
[ES2018]
$<name> capture of named group name
Example: Inserting the text before, inside, and after the matched substring.
exercises/regexps/change_quotes_test.mjs
by two.
assert.equal(
'3 cats and 4 dogs'.replaceAll(/[0-9]+/g, (all) =>
2 * Number(all)),
'6 cats and 8 dogs'
);
The replacement function gets the following parameters. Note how similar
they are to match objects. These parameters are all positional, but I’ve
Etc.
[ES2018]
groups : captures of named groups (an object). Always the last
parameter.
parameters. We access the last parameter via the Array method .at() in line
B.
In this section, we examine how the RegExp flags /g and /y work and how
interesting use case for .lastIndex that you may find surprising.
Every method reacts differently to /g and /y; this gives us a rough general
idea:
/g (.global, ES3): The regular expression should match multiple times,
anywhere in a string.
If a regular expression has neither the flag /g nor the flag /y, matching
inside the input string. That position is stored in the regular expression
property .lastIndex.
either /g or /y is set.
matching starts:
lines.
On the other hand, .lastIndex is set to one plus the last index of the
previous match.
This was a first overview. The next sections get into more details.
Without /g and /y, .exec() ignores .lastIndex and always returns a match
With /g, the match must start at .lastIndex or later. .lastIndex is updated. If
With /y, the match must start at exactly .lastIndex. .lastIndex is updated. If
This method behaves the same same as .exec(), but instead of returning a
match object, it returns true, and instead of returning null, it returns false.
For example, without either /g or /y, the result is always true:
Or with /y:
With /g, we get all matches (group 0) in an Array. .lastIndex is ignored and
reset to zero.
/yg works similarly to /g, but no gaps between matches are allowed:
/yg works similarly to /g, but no gaps between matches are allowed:
With /g, all occurrences are replaced. .lastIndex is ignored but reset to zero.
updated.
/yg works like /g, but gaps between matches are not allowed:
We will first look at four pitfalls of /g and /y and then at ways of dealing
while loop, the regular expression is created fresh, every time the condition
is checked. Therefore, its .lastIndex is always zero and the loop never
terminates.
let matchObj;
// Infinite loop
while (matchObj = /a+/g.exec('bbbaabaaa')) {
console.log(matchObj[0]);
}
If code expects a regular expression with /g and has a loop over the results
infinite loop:
the regular expression must not have /g. Otherwise, we generally get a
The first invocation produces a match and updates .lastIndex. The second
45.14.3.4 Pitfall 4: Code can produce unexpected results if .lastIndex isn’t zero
Given all the regular expression operations that are affected by .lastIndex,
won’t change it explicitly like we did in the example. But .lastIndex can still
end up not being zero if we use the regular expression multiple times.
let count = 0;
while (regExp.test(str)) {
count++;
}
return count;
}
Second, we can clone the parameter. That has the added benefit that regExp
won’t be changed.
let count = 0;
while (clone.test(str)) {
count++;
}
return count;
}
Apart from storing state, .lastIndex can also be used to start matching at a
Note that we must not use the assertion ^ which would anchor regExp to the
> '#--#'.search(/#/)
0
assert.equal(
searchAt(/#/g, '#--#', 0), 0);
assert.equal(
searchAt(/#/g, '#--#', 1), 3);
When used without /g and with /y, .replace() makes one replacement – if
For many use cases, we can’t make them immutable via freezing,
either.
operations.
String.prototype.search()
String.prototype.split()
/#/g.exec("##-#") {i:3} 4
/#/y.exec("##-#") null 0
/#/g.test("##-#") true 4
/#/y.test("##-#") false 0
"##-#".match(/#/g) ["#","#","#"] 0
"##-#".match(/#/y) null 0
"##-#".match(/#/gy) ["#","#"] 0
"##-#".matchAll(/#/g) <{i:3}> ✘
"##-#".matchAll(/#/y) TypeError ✘
"##-#".matchAll(/#/gy) <> ✘
Abbreviations:
classes ([···])):
function escapeForRegExp(str) {
return str.replace(/[\\^$.*+?\(\)\[\]\{\}\|]/gv,
'\\$&'); // (A)
}
assert.equal(escapeForRegExp('[yes?]'), String.raw`\
[yes\?\]`);
assert.equal(escapeForRegExp('_g_'),
String.raw`_g_`);
the regular expression flags /u and /v forbid many escapes – see “Syntax
We want to replace all occurrences of a plain text string via the regular
.replace() only lets us replace plain text once. With escapeForRegExp(), we can
🙂 🙂 🙂 🙂'
assert.equal(
':-) :-) :-)'.replace(regExp, ' '), '
);
inside character classes, you can take a look at the polyfill for the
> /(?:)/.test('')
true
> /(?:)/.test('abc')
true
> /.^/.test('')
false
> /.^/.test('abc')
false
comment. Therefore, the first of the previous two regular expressions is used
in this case:
easier to understand
This chapter describes JavaScript’s API for working with dates – the class
Date.
46.1 Best practice: avoid the built-in Date
The JavaScript Date API is cumbersome to use. Hence, it’s best to rely on a
Luxon
Day.js
js-joda
Moment.js
Libraries that use the built-in Date objects (for simpler use cases):
Tempo
date-fns
formatting.
Support for time zones: As explained later, Date does not support time
UTC, Z, and GMT are ways of specifying time that are similar, but subtly
different:
UTC (Coordinated Universal Time) is the time standard that all times
zones are based on. They are specified relative to it. That is, no country
and African countries. It is UTC plus zero hours and therefore has the
Sources:
UTC
Time offsets (relative to UTC)
Depending on the operation, only some of those options are available. For
the day of the month, you can only choose between the local time zone and
UTC.
Internally, Dates are stored as UTC. When converting from or to the local
time zone, the necessary offsets are determined via the date. In the
Whenever you create or convert dates, you need to be mindful of the time
standard being used – for example: new Date() uses the local time zone while
Dates interpret 0 as January. The day of the month is 27 in the local time
To be safe:
Use Z or a time offset when parsing strings (see the next section
Date.parse()
new Date()
Date.prototype.toISOString()
The following is an example of a date time string returned by .toISOString():
'2033-05-28T15:59:59.123Z'
YYYY-MM-DD
YYYY-MM
YYYY
THH:mm:ss.sss
THH:mm:ss.sssZ
THH:mm:ss
THH:mm:ssZ
THH:mm
THH:mmZ
UTC:
THH:mm+HH:mm (etc.)
THH:mm-HH:mm (etc.)
If you add a Z to the end of a string, date parsing doesn’t produce different
const timeValue = 0;
assert.equal(
new Date(timeValue).toISOString(),
'1970-01-01T00:00:00.000Z');
Coercing a Date to a number returns its time value:
assert.equal(
new Date('1972-05-03') < new Date('2001-12-23'),
true);
// Internally:
assert.equal(73699200000 < 1009065600000, true);
offset)
number (UTC)
Returns the time value for the specified UTC date time.
Date.prototype.setTime(timeValue) (UTC)
new Date(year: number, month: number, date?: number, hours?: number, minutes?:
That’s why, elsewhere in this chapter, we avoid the time unit year and
Example:
> new Date(2077,0,27, 21,49).toISOString() // CET
(UTC+1)
'2077-01-27T20:49:00.000Z'
Note that the input hours (21) are different from the output hours (20). The
If there is no Z or time offset at the end, the local time zone is used:
Dates have getters and setters for time units – for example:
Date.prototype.getFullYear()
Date.prototype.setFullYear(num)
Date.prototype.get«Unit»()
Date.prototype.set«Unit»(num)
UTC:
Date.prototype.getUTC«Unit»()
Date.prototype.setUTC«Unit»(num)
Date
FullYear
Time
There is one more getter that doesn’t conform to the previously mentioned
patterns:
Date.prototype.getTimezoneOffset()
Returns the time difference between local time zone and UTC in
Example Date:
> d.toDateString()
'Thu Jan 01 1970'
> d.toString()
'Thu Jan 01 1970 01:00:00 GMT+0100 (Central
European Standard Time)'
Date.prototype.toUTCString() (UTC)
> d.toUTCString()
'Thu, 01 Jan 1970 00:00:00 GMT'
Date.prototype.toISOString() (UTC)
> d.toISOString()
'1970-01-01T00:00:00.000Z'
functionality for formatting dates (including support for time zones), but not
Date.prototype.toLocaleTimeString()
Date.prototype.toLocaleDateString()
Date.prototype.toLocaleString()
exercises/dates/create_date_string_test.mjs
47 Creating and parsing JSON (JSON)
stringify
47.5 FAQ
{
"first": "Jane",
"last": "Porter",
"married": true,
"born": 1890,
"friends": [ "Tarzan", "Cheeta" ]
}
JavaScript has the global namespace object JSON that provides methods for
json.org. He explains:
already existed in nature. What I did was I found it, I named it, I
described how it was useful. I don't claim to be the first person to have
discovered it; I know that there are other people who discovered it at
least a year before I did. The earliest occurrence I've found was, there
was someone at Netscape who was using JavaScript array literals for
stability.
they are considered desirable. However, that still leaves room for creating
Compound:
Object literals:
Array literals:
Atomic:
Booleans
data.
If you only provide the first argument, .stringify() returns a single line of
text:
assert.equal(
JSON.stringify({foo: ['a', 'b']}),
'{"foo":["a","b"]}' );
If you provide a non-negative integer for space, then .stringify() returns one
assert.equal(
JSON.stringify({foo: ['a', 'b']}, null, 2),
`{
"foo": [
"a",
"b"
]
}`);
Primitive values:
> JSON.stringify('abc')
'"abc"'
> JSON.stringify(123)
'123'
> JSON.stringify(null)
'null'
> JSON.stringify(NaN)
'null'
> JSON.stringify(Infinity)
'null'
Bigints: TypeError
> JSON.stringify(123n)
TypeError: Do not know how to serialize a BigInt
Objects:
stringified:
> JSON.parse('{"foo":["a","b"]}')
{ foo: [ 'a', 'b' ] }
JSON.
class Point {
static fromJson(jsonObj) { // (A)
return new Point(jsonObj.x, jsonObj.y);
}
constructor(x, y) {
this.x = x;
this.y = y;
}
toJSON() { // (B)
return {x: this.x, y: this.y};
}
}
assert.deepEqual(
Point.fromJson(JSON.parse('{"x":3,"y":5}')),
new Point(3, 5) );
assert.equal(
JSON.stringify(new Point(3, 5)),
'{"x":3,"y":5}' );
before it is stringified.
JSON.parse(text, reviver?)
properties, whose names are mentioned there, are included in the result:
const obj = {
a: 1,
b: {
c: 2,
d: 3,
}
};
assert.equal(
JSON.stringify(obj, ['b', 'c']),
'{"b":{"c":2}}');
data is atomic, it is a tree that only has a root. All values in the tree are fed
to the value visitor, one at a time. Depending on what the visitor returns, the
this: Parent of current value. The parent of the root value r is {'': r}.
Note: this is an implicit parameter and only available if the value
key: Key or index of the current value inside its parent. The key of the
tree.
The following code shows in which order a value visitor sees values:
const root = {
a: 1,
b: {
c: 2,
d: 3,
}
};
JSON.stringify(root, valueVisitor);
assert.deepEqual(log, [
{ this: { '': root }, key: '', value: root },
{ this: root , key: 'a', value: 1 },
{ this: root , key: 'b', value: root.b },
{ this: root.b , key: 'c', value: 2 },
{ this: root.b , key: 'd', value: 3 },
]);
first, leaves last). The rationale for going in that direction is that we are
root last). The rationale for going in that direction is that we are assembling
JSON values into JavaScript values. Therefore, we need to convert the parts
const obj = {
name: 'abc',
regex: /abc/ui,
};
assert.equal(
JSON.stringify(obj),
'{"name":"abc","regex":{}}');
47.5 FAQ
Suppose you are using JSON to keep configuration files, which you
would like to annotate. Go ahead and insert all the comments you like.
development
JavaScript we deploy
48.3.3 Testing
48.3.5 Libraries
You now know most of the JavaScript language. This chapter gives an
such as:
How can you avoid feeling overwhelmed when faced with this constantly
Focus on the web technologies that you work with most often and learn
For JavaScript: Know the language, but also try out one tool in each of
normal JavaScript.
wary of trying to learn everything and spreading oneself too thin. That
Svelte, Vue.js.
servers. But they are also used for running command-line tools. The
via npm. A good way to get started with Node.js, is to use it for shell
scripting.
ecosystem.
web apps is to give web apps features that, traditionally, only native
The app must be served over HTTPS (not the unsecure HTTP).
The app must have a Web App Manifest file, specifying metadata
The app must have a service worker: a base layer of the app that
all of the web resource requests of the app. And it has access to a
(source). Use cases include support for new video formats, machine
another language?
result would be slower than current engines and have a similar code base.
foreseeable future, it will probably remain the most popular choice for high-
level code. For low-level code, compiling more static languages (such as
Given that it is just a virtual machine, there are not that many practically
embedded devices.”
and environments.”
Categories of tools
The former are much more important. The names change, as tools come into
and out of style, but I wanted you to see at least some of them.
JavaScript we deploy
JavaScript we deploy. The following tools are often involved in this process.
48.3.1.1 Transpilers
two tasks:
same).
One important trend is to use faster tools (such as bundlers, see below)
versions of the language. That means we can use new features in our
48.3.1.2 Minification
let a=5;Math.random()&&a++;
48.3.1.3 Bundlers
Bundlers compile and optimize the code of a JavaScript app. The input of a
bundler is many files – all of the app’s code plus the libraries it uses. A
bundler combines these input files to produce fewer output files (which
A bundler minimizes the size of its output via techniques such as tree-
module exports are put in the output that are imported somewhere (across
Vite
More like a library (we control the tool):
esbuild
Rollup
Rolldown
Somewhere between:
Parcel
Rspack
TurboPack
webpack
Sometimes there are build tasks that involve multiple tools or invoke tools
with specific arguments. Task runners (in the tradition of Unix make) let us
define simpler names for such tasks and often also help with connecting
package.json.
Most runtimes also come with built-in support for running tasks.
Static checking means analyzing source code statically (without running it).
variables, etc. Linters are especially useful if you are still learning the
language because they point out if you are doing something wrong.
48.3.3 Testing
The JavaScript runtimes Node.js, Deno and Bun all have built-in
test runners.
package manager for Node.js but has since also become dominant for client-
database):
(including npm).
Support for it is built into Deno. JSR was created by the team behind
48.3.5 Libraries
Data structures: The following libraries are two examples among many.
also doesn’t mutate the data it operates on, but it works with
Date libraries: JavaScript’s built-in support for dates is limited and full
of pitfalls. The chapter on dates lists libraries that we can use instead.
modern browsers.
Given that JavaScript is just one of several kinds of artifacts involved in web
CSS:
--x, #1
x--, #1
-x, #1
, (comma operator), #1
!x, #1
c ? t : e, #1
obj?.prop, #1
.__proto__, #1
x - y, #1
x ??= y, #1
x ?? d, #1
x ** y, #1
x * y, #1
x / y, #1
x && y, #1
x & y, #1
x ٪ y, #1
x ^ y, #1
x + y, #1
x << y, #1
x === y, #1
x >>> y, #1
x >> y, #1
x ¦¦ y, #1
x ¦ y, #1
++x, #1
x++, #1
+x, #1
=, #1
~x, #1
addition, #1
AggregateError, #1
AMD module, #1
argument, #1
Array, #1
Array hole, #1
Array index, #1
Array literal, #1
Array-destructuring, #1
Array-like object, #1
Array, dense, #1
Array, multidimensional, #1
new Array(), #1
Array, sparse, #1
Array.from(), #1
Array.of(), #1
Array.prototype.at(), #1
Array.prototype.concat(), #1
Array.prototype.copyWithin(), #1
Array.prototype.entries(), #1
Array.prototype.every(), #1
Array.prototype.fill(), #1
Array.prototype.filter(), #1
Array.prototype.find(), #1
Array.prototype.findIndex(), #1
Array.prototype.findLast(), #1
Array.prototype.findLastIndex(), #1
Array.prototype.flat(), #1
Array.prototype.flatMap(), #1
Array.prototype.forEach(), #1
Array.prototype.includes(), #1
Array.prototype.indexOf(), #1
Array.prototype.join(), #1
Array.prototype.keys(), #1
Array.prototype.lastIndexOf(), #1
Array.prototype.map(), #1
Array.prototype.pop(), #1
Array.prototype.push(), #1
Array.prototype.reduce(), #1
Array.prototype.reduceRight(), #1
Array.prototype.reverse(), #1
Array.prototype.shift(), #1
Array.prototype.slice(), #1
Array.prototype.some(), #1
Array.prototype.sort(), #1
Array.prototype.splice(), #1
Array.prototype.toLocaleString(), #1
Array.prototype.toReversed(), #1
Array.prototype.toSorted(), #1
Array.prototype.toSpliced(), #1
Array.prototype.toString(), #1
Array.prototype.unshift(), #1
Array.prototype.values(), #1
Array.prototype.with(), #1
ArrayBuffer, #1
new ArrayBuffer(), #1
ArrayBuffer.isView(), #1
ArrayBuffer.prototype.byteLength, #1
ArrayBuffer.prototype.maxByteLength, #1
ArrayBuffer.prototype.resizable, #1
ArrayBuffer.prototype.resize(), #1
ArrayBuffer.prototype.slice(), #1
Arrays, fixed-layout, #1
Arrays, sequence, #1
arrow function, #1
ASCII escape, #1
assert (module), #1
assert.deepEqual(), #1
assert.equal(), #1
assert.fail(), #1
assert.notDeepEqual(), #1
assert.notEqual(), #1
assert.throws(), #1
assertion, #1
assignment operator, #1
async, #1
async function, #1
async function*, #1
async-await, #1
asynchronous generator, #1
asynchronous iterable, #1
asynchronous iteration, #1
asynchronous iterator, #1
asynchronous programming, #1
attribute of a property, #1
base class, #1
big endian, #1
bigint, #1
BigInt64Array, #1
BigUint64Array, #1
binding (variable), #1
bitwise And, #1
bitwise Not, #1
bitwise Or, #1
bitwise Xor, #1
boolean, #1
Boolean(), #1
bound variable, #1
break, #1
bundler, #1
bundling, #1
call stack, #1
callback function, #1
case, camel, #1
case, dash, #1
case, kebab, #1
case, snake, #1
case, underscore, #1
catch, #1
class, #1, #2
class declaration, #1
class definition, #1
class expression, #1
class, base, #1
class, derived, #1
class, mixin, #1
classes, extending, #1
closure, #1
code point, #1
code unit, #1
coercion, #1
comma operator, #1
CommonJS module, #1
comparing by identity, #1
comparing by value, #1
concatenating strings, #1
conditional operator, #1
console, #1
console.error(), #1
console.log(), #1
const, #1
constant, #1
continue, #1
Converting to [type], #1
DataView, #1
new DataView(), #1
DataView.prototype.buffer, #1
DataView.prototype.byteLength, #1
DataView.prototype.byteOffset, #1
DataView.prototype.get(), #1
DataView.prototype.set(), #1
date, #1
default export, #1
delete, #1
deleting a property, #1
dense Array, #1
derived class, #1
descriptor of a property, #1
destructive operation, #1
destructuring, #1
destructuring an Array, #1
destructuring an object, #1
dictionary object, #1
divided by operator, #1
division, #1
do-while, #1
dynamic imports, #1
dynamic this, #1
early activation, #1
Ecma, #1
ECMA-262, #1
ECMAScript, #1
ECMAScript module, #1
ECMAScript proposal, #1
Eich, Brendan, #1
enumerability, #1
equality operator, #1
ES module, #1
escape, ASCII, #1
escaping HTML, #1
eval(), #1
evaluating an expression, #1
event loop, #1
exception, #1
exponentiation, #1
export, #1
export default, #1
export, default, #1
export, named, #1
expression, #1
extending classes, #1
extends, #1
external iteration, #1
extracting a method, #1
false, #1
falsiness, #1
falsy, #1
finally, #1
fixed-layout Arrays, #1
fixed-layout object, #1
Float32Array, #1
Float64Array, #1
for, #1
for-await-of, #1
for-in, #1
for-of, #1
free variable, #1
function declaration, #1
function, arrow, #1
function, ordinary, #1
function, specialized, #1
function*, #1
garbage collection, #1
generator, asynchronous, #1
generator, synchronous, #1
global object, #1
global scope, #1
global variable, #1
globalThis, #1
grapheme cluster, #1
heap, #1
hoisting, #1
hole in an Array, #1
identifier, #1
identity of an object, #1
if, #1
import, #1
import, named, #1
import, namespace, #1
import.meta, #1
import.meta.url, #1
import(), #1
imports, dynamic, #1
in, #1
Infinity, #1
inheritance, multiple, #1
inheritance, single, #1
instanceof, #1, #2
Int8Array, #1
Int16Array, #1
Int32Array, #1
integer numbers, #1
integer, safe, #1
internal iteration, #1
iterable (asynchronous), #1
iterable (synchronous), #1
iteration, asynchronous, #1
iteration, external, #1
iteration, internal, #1
iteration, synchronous, #1
iterator (asynchronous), #1
iterator (synchronous), #1
keyword, #1
label, #1
leading surrogate, #1
lexical this, #1
listing properties, #1
little endian, #1
logical And, #1
logical Not, #1
logical Or, #1
lone surrogate, #1
Map, #1
new Map(), #1
Map.groupBy(), #1
Map.prototype.clear(), #1
Map.prototype.delete(), #1
Map.prototype.entries(), #1
Map.prototype.forEach(), #1
Map.prototype.get(), #1
Map.prototype.has(), #1
Map.prototype.keys(), #1
Map.prototype.set(), #1
Map.prototype.size, #1
Map.prototype.values(), #1
Map.prototype[Symbol.iterator](), #1
method, #1
method, extracting a, #1
minification, #1
minifier, #1
mixin class, #1
module specifier, #1
module, AMD, #1
module, CommonJS, #1
multidimensional Array, #1
multiple inheritance, #1
multiplication, #1
named export, #1
named import, #1
named parameter, #1
namespace import, #1
NaN, #1
node_modules, #1
non-destructive operation, #1
npm, #1
npm package, #1
null, #1
number, #1
Number.EPSILON(), #1
Number.isFinite(), #1
Number.isInteger(), #1
Number.isNaN(), #1
Number.isSafeInteger(), #1
Number.MAX_SAFE_INTEGER(), #1
Number.MAX_VALUE(), #1
Number.MIN_SAFE_INTEGER(), #1
Number.MIN_VALUE(), #1
Number.NaN(), #1
Number.NEGATIVE_INFINITY(), #1
Number.parseFloat(), #1
Number.parseInt(), #1
Number.POSITIVE_INFINITY(), #1
Number.prototype.toExponential(), #1
Number.prototype.toFixed(), #1
Number.prototype.toPrecision(), #1
Number.prototype.toString(), #1
Number(), #1
object, #1
object literal, #1
object-destructuring, #1
object, dictionary, #1
object, fixed-layout, #1
object, identity of an, #1
Object.assign(), #1
Object.create(), #1
Object.defineProperties(), #1
Object.defineProperty(), #1
Object.entries(), #1
Object.freeze(), #1
Object.fromEntries(), #1
Object.getOwnPropertyDescriptor(), #1
Object.getOwnPropertyDescriptors(), #1
Object.getOwnPropertyNames(), #1
Object.getOwnPropertySymbols(), #1
Object.getPrototypeOf(), #1
Object.groupBy(), #1
Object.hasOwn(), #1
Object.is(), #1, #2
Object.isExtensible(), #1
Object.isFrozen(), #1
Object.isSealed(), #1
Object.keys(), #1
Object.preventExtensions(), #1
Object.prototype methods, #1
Object.prototype.__proto__, #1
Object.prototype.hasOwnProperty(), #1
Object.prototype.isPrototypeOf(), #1
Object.prototype.propertyIsEnumerable(), #1
Object.prototype.toLocaleString(), #1
Object.prototype.toString(), #1
Object.prototype.valueOf(), #1
Object.seal(), #1
Object.setPrototypeOf(), #1
Object.values(), #1
Object(), #1
ones’ complement, #1
operator, assignment, #1
operator, comma, #1
operator, equality, #1
operator, void, #1
ordinary function, #1
overriding a property, #1
package, npm, #1
package.json, #1
parameter, #1
passing by identity, #1
passing by value, #1
polyfill, #1
polyfill, speculative, #1
ponyfill, #1
primitive value, #1
private name, #1
private slot, #1
prollyfill, #1
Promise, #1, #2
Promise, states of a, #1
Promise.all(), #1, #2
Promise.allSettled(), #1, #2
Promise.any(), #1, #2
Promise.race(), #1, #2
properties, listing, #1
property, #1
property (object), #1
property attribute, #1
property descriptor, #1
property key, #1
property name, #1
property symbol, #1
prototype, #1
prototype chain, #1
public slot, #1
receiver, #1
Reflect.apply(), #1
Reflect.construct(), #1
Reflect.defineProperty(), #1
Reflect.deleteProperty(), #1
Reflect.get(), #1
Reflect.getOwnPropertyDescriptor(), #1
Reflect.getPrototypeOf(), #1
Reflect.has(), #1
Reflect.isExtensible(), #1
Reflect.ownKeys(), #1
Reflect.preventExtensions(), #1
Reflect.set(), #1
Reflect.setPrototypeOf(), #1
RegExp, #1
regular expression, #1
remainder operator, #1
REPL, #1
replica, #1
RequireJS, #1
reserved word, #1
run-to-completion semantics, #1
safe integer, #1
scope of a variable, #1
script, #1
self, #1
sequence Arrays, #1
Set, #1, #2
new Set(), #1
Set.prototype.add(), #1
Set.prototype.clear(), #1
Set.prototype.delete(), #1
Set.prototype.forEach(), #1
Set.prototype.has(), #1
Set.prototype.size, #1
Set.prototype.values(), #1
Set.prototype[Symbol.iterator](), #1
shim, #1
short-circuiting, #1
single inheritance, #1
sloppy mode, #1
slot, private, #1
slot, public, #1
sparse Array, #1
specialized function, #1
specifier, module, #1
speculative polyfill, #1
statement, #1
states of a Promise, #1
static, #1
strict mode, #1
string, #1
String.prototype.at(), #1
String.prototype.concat(), #1
String.prototype.endsWith(), #1
String.prototype.includes(), #1
String.prototype.indexOf(), #1
String.prototype.isWellFormed(), #1
String.prototype.lastIndexOf(), #1
String.prototype.match(), #1
String.prototype.normalize(), #1
String.prototype.padEnd(), #1
String.prototype.padStart(), #1
String.prototype.repeat(), #1
String.prototype.replace(), #1
String.prototype.replaceAll(), #1
String.prototype.search(), #1
String.prototype.slice(), #1
String.prototype.split(), #1
String.prototype.startsWith(), #1
String.prototype.substring(), #1
String.prototype.toLowerCase(), #1
String.prototype.toUpperCase(), #1
String.prototype.toWellFormed(), #1
String.prototype.trim(), #1
String.prototype.trimEnd(), #1
String.prototype.trimStart(), #1
String(), #1
subclass, #1
subclassing, #1
subtraction, #1
superclass, #1
surrogate, leading, #1
surrogate, lone, #1
surrogate, trailing, #1
switch, #1
symbol, #1
synchronous generator, #1
synchronous iterable, #1
synchronous iteration, #1
synchronous iterator, #1
syntax, #1
tagged template, #1
task queue, #1
task runner, #1
TC39, #1
TC39 process, #1
template literal, #1
ternary operator, #1
this, #1
this, dynamic, #1
this, lexical, #1
throw, #1
time value, #1
times operator, #1
trailing surrogate, #1
transpilation, #1
transpiler, #1
tree-shaking, #1
true, #1
truthiness, #1
truthy, #1
try, #1
type, #1
type hierarchy, #1
type signature, #1
Typed Array, #1
TypedArray.from(), #1
TypedArray.of(), #1
TypedArray.prototype.buffer, #1
TypedArray.prototype.byteLength, #1
TypedArray.prototype.byteOffset, #1
TypedArray.prototype.length, #1
TypedArray.prototype.set(), #1
TypedArray.prototype.subarray(), #1
typeof, #1
TypeScript, #1
Uint8Array, #1
Uint8ClampedArray, #1
Uint16Array, #1
Uint32Array, #1
undefined, #1
Unicode, #1
unit test, #1
UTF-8, #1
UTF-16, #1
UTF-32, #1
value-preservation, #1
variable, bound, #1
variable, free, #1
variable, scope of a, #1
void operator, #1
Wasm (WebAssembly), #1
WeakMap, #1, #2
WeakSet, #1, #2
Web Worker, #1
WebAssembly, #1
while, #1
window, #1