Dokumen - Pub - Javascript For Impatient Programmers Z 5657019
Dokumen - Pub - Javascript For Impatient Programmers Z 5657019
programmers
Dr. Axel Rauschmayer
2019
JavaScript for impatient
programmers
JavaScript for impatient programmers
1 About this book (ES2019 edition)
1.1 About the content
1.2 Previewing and buying this book
1.3 About the author
1.4 Acknowledgements
2 FAQ: Book and supplementary material
2.1 How to read this book
2.2 I own a digital edition
2.3 I own the print edition
2.4 Notations and conventions
6 FAQ: JavaScript
6.1 What are good references for JavaScript?
6.2 How do I find out what JavaScript features are supported
where?
6.3 Where can I look up what features are planned for
JavaScript?
6.4 Why does JavaScript fail silently so often?
6.5 Why can’t we clean up JavaScript, by removing quirks and
outdated features?
6.6 How can I quickly try out a piece of JavaScript code?
7 The big picture
7.1 What are you learning in this book?
7.2 The structure of browsers and Node.js
7.3 JavaScript references
7.4 Further reading
8 Syntax
8.1 An overview of JavaScript’s syntax
8.2 (Advanced)
8.3 Identifiers
8.4 Statement vs. expression
8.5 Ambiguous syntax
8.6 Semicolons
8.7 Automatic semicolon insertion (ASI)
8.8 Semicolons: best practices
8.9 Strict mode vs. sloppy mode
10 Assertion API
10.1 Assertions in software development
10.2 How assertions are used in this book
10.3 Normal comparison vs. deep comparison
10.4 Quick reference: module assert
13 Values
13.1 What’s a type?
13.2 JavaScript’s type hierarchy
13.3 The types of the language specification
13.4 Primitive values vs. objects
13.5 The operators typeof and instanceof: what’s the type of a
value?
13.6 Classes and constructor functions
13.7 Converting between types
14 Operators
14.1 Making sense of operators
14.2 The plus operator (+)
14.3 Assignment operators
14.4 Equality: == vs. ===
14.5 Ordering operators
14.6 Various other operators
16 Booleans
16.1 Converting to boolean
16.2 Falsy and truthy values
16.3 Truthiness-based existence checks
16.4 Conditional operator (? :)
16.5 Binary logical operators: And (x && y), Or (x || y)
16.6 Logical Not (!)
17 Numbers
17.1 JavaScript only has floating point numbers
17.2 Number literals
17.3 Arithmetic operators
17.4 Converting to number
17.5 Error values
17.6 Error value: NaN
17.7 Error value: Infinity
17.8 The precision of numbers: careful with decimal fractions
17.9 (Advanced)
17.10 Background: floating point precision
17.11 Integers in JavaScript
17.12 Bitwise operators
17.13 Quick reference: numbers
18 Math
18.1 Data properties
18.2 Exponents, roots, logarithms
18.3 Rounding
18.4 Trigonometric Functions
18.5 Various other functions
18.6 Sources
20 Strings
20.1 Plain string literals
20.2 Accessing characters and code points
20.3 String concatenation via +
20.4 Converting to string
20.5 Comparing strings
20.6 Atoms of text: Unicode characters, JavaScript characters,
grapheme clusters
20.7 Quick reference: Strings
22 Symbols
22.1 Use cases for symbols
22.2 Publicly known symbols
22.3 Converting symbols
24 Exception handling
24.1 Motivation: throwing and catching exceptions
24.2 throw
24.3 The try statement
24.4 Error classes
25 Callable values
25.1 Kinds of functions
25.2 Ordinary functions
25.3 Specialized functions
25.4 More kinds of functions and methods
25.5 Returning values from functions and methods
25.6 Parameter handling
25.7 Dynamically evaluating code: eval(), new Function()
(advanced)
26 Environments: under the hood of variables (bonus)
26.1 Environment: data structure for managing variables
26.2 Recursion via environments
26.3 Nested scopes via environments
26.4 Closures and environments
27 Modules
27.1 Overview: syntax of ECMAScript modules
27.2 JavaScript source code formats
27.3 Before we had modules, we had scripts
27.4 Module systems created prior to ES6
27.5 ECMAScript modules
27.6 Named exports and imports
27.7 Default exports and imports
27.8 More details on exporting and importing
27.9 npm packages
27.10 Naming modules
27.11 Module specifiers
27.12 Loading modules dynamically via import()
27.13 Preview: import.meta.url
27.14 Polyfills: emulating native web platform features
(advanced)
28 Single objects
28.1 What is an object?
28.2 Objects as records
28.3 Spreading into object literals (...)
28.4 Methods
28.5 Objects as dictionaries (advanced)
28.6 Standard methods (advanced)
28.7 Advanced topics
36 WeakSets (WeakSet)
36.1 Example: Marking objects as safe to use with a method
36.2 WeakSet API
37 Destructuring
37.1 A first taste of destructuring
37.2 Constructing vs. extracting
37.3 Where can we destructure?
37.4 Object-destructuring
37.5 Array-destructuring
37.6 Examples of destructuring
37.7 What happens if a pattern part does not match anything?
37.8 What values can’t be destructured?
37.9 (Advanced)
37.10 Default values
37.11 Parameter definitions are similar to destructuring
37.12 Nested destructuring
42 Asynchronous iteration
42.1 Basic asynchronous iteration
42.2 Asynchronous generators
42.3 Async iteration over Node.js streams
47 Index
JavaScript for impatient
programmers
1 About this book (ES2019
edition)
Highlights:
There are several ways in which you can read this book. One of them
involves skipping much of the content in order to get started quickly.
For details, see §2.1.1 “In which order should I read the content in
this book?”.
1.2 Previewing and buying this
book
1.2.1 How can I preview the book, the
exercises, and the quizzes?
The home page of this book describes how you can buy them.
This chapter answers questions you may have and gives tips for
reading this book.
2.1 How to read this book
2.1.1 In which order should I read the
content in this book?
As your knowledge evolves, you can later come back to some or all of
the advanced content.
The bonus chapters are only available in the paid versions of this
book (print and ebook). They are listed in the full table of contents.
2.2 I own a digital edition
2.2.1 How do I submit feedback and
corrections?
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 able to download the latest version of the files at that
location.
Yes. The instructions for doing so are on the homepage of this book.
2.3 I own the print edition
2.3.1 Can I get a discount for a digital
edition?
If you bought the print edition, you can get a discount for a digital
edition. The homepage of the print edition explains how.
Alas, the reverse is not possible: you cannot get a discount for the
print edition if you bought a digital edition.
On the homepage of the print edition, you can submit errors and see
submitted errors.
The homepage of the print edition has a list with all the URLs that
you see in the footnotes of the print edition.
2.4 Notations and conventions
2.4.1 What is a type signature? Why am I
seeing static types in this book?
Why is this notation being used? It helps give you a quick idea of how
a function works. The notation is explained in detail in a 2ality blog
post, but is usually relatively intuitive.
Reading instructions
External content
Question
Warning
Details
Exercise
Quiz
3.2.1 Community
With JavaScript, you can write apps for many client platforms. These
are a few example technologies:
3.2.3 Language
Quiz
> 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.
4.3 Tips for getting started with
JavaScript
These are a few tips to help you get started with JavaScript:
The idea was that major interactive parts of the client-side web were
to be implemented in Java. JavaScript was supposed to be a glue
language for 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 solutions such as Perl, Python, TCL,
and others.
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.
Pick champions
Spec complete
If you are wondering what stages various proposed features are in,
consult the GitHub repository proposals.
Yes, the TC39 repo lists finished proposals and mentions in which
ECMAScript versions they were introduced.
5.7 Evolving JavaScript: Don’t
break the web
One idea that occasionally comes up is to clean up JavaScript by
removing old features and quirks. While the appeal of that idea is
obvious, it has significant downsides.
Quiz
> 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.
6.5 Why can’t we clean up
JavaScript, by removing quirks and
outdated features?
This question is answered in §5.7 “Evolving JavaScript: Don’t break
the web”.
6.6 How can I quickly try out a
piece of JavaScript code?
§9.1 “Trying out JavaScript code” explains how to do that.
7 The big picture
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?
7.1 What are you learning in this
book?
This book teaches the JavaScript language. It focuses on just the
language, but offers occasional glimpses at two platforms where
JavaScript can be used:
Web browser
Node.js
Comments:
// single-line comment
/*
Comment with
multiple lines
*/
// Booleans
true
false
assert.equal(7 + 1, 8);
assert.equal() is a method call (the object is assert, the method is
.equal()) with two arguments: the actual result and the expected
result. It is part of a Node.js assertion API that is explained later in
this book.
Operators:
// Comparison operators
assert.equal(3 < 4, true);
assert.equal(3 <= 4, true);
assert.equal('abc' === 'abc', true);
assert.equal('abc' !== 'def', true);
Declaring variables:
let x; // declaring x (mutable)
x = 3 * 5; // assign a value to x
// Conditional statement
if (x < 0) { // is x less than zero?
x = -x;
}
// Equivalent to add2:
const add3 = (a, b) => a + b;
The previous code contains the following two arrow functions (the
terms expression and statement are explained later in this chapter):
Objects:
8.1.2 Modules
Each module is a single file. Consider, for example, the following two
files with modules in them:
file-tools.mjs
main.mjs
The module in main.mjs imports the whole module path and the
function isTextFilePath():
Lowercase:
Uppercase:
Classes: MyClass
Constants: MY_CONSTANT
Constants are also often written in camel case: myConstant
arr.map((_x, i) => i)
class ValueWrapper {
constructor(value) {
this._value = value;
}
}
const x = 123;
func();
while (false) {
// ···
} // no semicolon
function func() {
// ···
} // no semicolon
Quiz: basic
First character:
Subsequent characters:
Examples:
const ε = 0.0001;
const строка = '';
let _tmp = 0;
const $foo2 = true;
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, because they effectively are keywords:
You shouldn’t use the names of global variables (String, Math, etc.)
for your own variables and parameters, either.
8.4 Statement vs. expression
In this section, we explore how JavaScript distinguishes two kinds of
syntactic constructs: statements and expressions. Afterward, we’ll
see that that can cause problems because the same syntax can mean
different things, depending on where it is used.
8.4.1 Statements
let myStr;
if (myBool) {
myStr = 'Yes';
} else {
myStr = 'No';
}
function twice(x) {
return x + x;
}
8.4.2 Expressions
An expression is a piece of code that can be evaluated to produce a
value. For example, the code between the parentheses is an
expression:
function max(x, y) {
if (x > y) {
return x;
} else {
return y;
}
}
The arguments of a function call or a method call must be
expressions:
function f() {
console.log(bar()); // bar() is expression
bar(); // bar(); is (expression) statement
}
8.5 Ambiguous syntax
JavaScript has several programming constructs that are syntactically
ambiguous: the same syntax is interpreted differently, depending on
whether it is used in statement context or in expression context. This
section explores the phenomenon and the pitfalls it causes.
function id(x) {
return x;
}
8.5.3 Disambiguation
// Output:
// 'abc'
In this code:
const x = 3;
someFunction('abc');
i++;
function foo() {
// ···
}
if (y > 0) {
// ···
}
while (condition)
statement
But blocks are also statements and therefore legal bodies of control
statements:
while (a > 0) {
a--;
}
A semicolon
A line terminator followed by an illegal token
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 break, a semicolon will be inserted, too.
return
{
first: 'jane'
};
return;
{
first: 'jane';
}
;
That is:
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 when a
statement ends.
There are less rules to keep in mind.
The majority of JavaScript programmers use semicolons.
However, there are also many people who don’t like the added visual
clutter of semicolons. If you are one of them: Code without them is
legal. I recommend that you use tools to help you avoid mistakes.
The following are two examples:
In script files and CommonJS modules, you switch on strict mode for
a complete file, by putting the following code in the first line:
'use strict';
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.
function sloppyFunc() {
undeclaredVar1 = 123;
}
sloppyFunc();
// Created global variable `undeclaredVar1`:
assert.equal(undeclaredVar1, 123);
function strictFunc() {
'use strict';
undeclaredVar2 = 123;
}
assert.throws(
() => strictFunc(),
{
name: 'ReferenceError',
message: 'undeclaredVar2 is not defined',
});
The assert.throws() states that its first argument, a function, throws
a ReferenceError when it is called.
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);
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);
Quiz: advanced
To find out how to open the console in your web browser, you can do
a web search for “console «name-of-your-browser»”. These are pages
for a few commonly used web browsers:
Apple Safari
Google Chrome
Microsoft Edge
Mozilla Firefox
Figure 3: The console of the web browser “Google Chrome” is open
(in the bottom half of window) while visiting a web page.
> 3 + 5
8
There are many web apps that let you experiment with
JavaScript in web browsers – for example, Babel’s REPL.
There are also native apps and IDE plugins for running
JavaScript.
The full console.* API is documented on MDN web docs and on the
Node.js website. It is not part of the JavaScript language standard,
but much functionality is supported by both browsers and Node.js.
console.log()
console.error()
These are some of the directives you can use for substitutions:
%% inserts a single %.
console.log('%s%%', 99);
// Output:
// 99%
Output:
{
"first": "Jane",
"last": "Doe"
}
10 Assertion API
function id(x) {
return x;
}
assert.equal(id('abc'), 'abc');
For more information, consult §11 “Getting started with quizzes and
exercises”.
10.3 Normal comparison vs. deep
comparison
The strict equal() uses === to compare values. Therefore, an object is
only equal to itself – even if another object has the same content
(because === does not compare the contents of objects, only their
identities):
assert.equal(3+3, 6);
assert.notEqual(3+3, 22);
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');
}
assert.deepEqual([1,2,3], [1,2,3]);
assert.deepEqual([], []);
assert.notDeepEqual([1,2,3], [1,2]);
If you want to (or expect to) receive an exception, you need throws():
This function calls its first parameter, the function block, and only
succeeds if it throws an exception. Additional parameters can be
used to specify what that exception must look like.
assert.throws(
() => {
null.prop;
},
TypeError
);
assert.throws(
() => {
null.prop;
},
/^TypeError: Cannot read property 'prop' of null$/
);
assert.throws(
() => {
null.prop;
},
{
name: 'TypeError',
message: `Cannot read property 'prop' of null`,
}
);
try {
functionThatShouldThrow();
assert.fail();
} catch (_) {
// Success
}
Quiz
11.1 Quizzes
11.2 Exercises
11.2.1 Installing the exercises
11.2.2 Running exercises
11.3 Unit tests in JavaScript
11.3.1 A typical test
11.3.2 Asynchronous tests in AVA
The key thing here is: everything you want to test must be exported.
Otherwise, the test code can’t access it.
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, but not write them.
// npm t demos/quizzes-exercises/id_test.mjs
npm t demos/quizzes-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/quizzes-exercises/first_module_test.mjs
Reading
You can postpone reading this section until you get to the chapters
on asynchronous programming.
Writing tests for asynchronous code requires extra work: The test
receives its results later and has to signal to AVA that it isn’t finished
yet when it returns. The following subsections examine three ways of
doing so.
11.3.2.1 Asynchronicity via callbacks
test.cb('divideCallback', t => {
divideCallback(8, 4, (error, result) => {
if (error) {
t.end(error);
} else {
assert.strictEqual(result, 2);
t.end();
}
});
});
12.1 let
12.2 const
12.2.1 const and immutability
12.2.2 const and loops
12.3 Deciding between const and let
12.4 The scope of a variable
12.4.1 Shadowing variables
12.5 (Advanced)
12.6 Terminology: static vs. dynamic
12.6.1 Static phenomenon: scopes of variables
12.6.2 Dynamic phenomenon: function calls
12.7 Global variables and the global object
12.7.1 globalThis
12.8 Declarations: scope and activation
12.8.1 const and let: temporal dead zone
12.8.2 Function declarations and early activation
12.8.3 Class declarations are not activated early
12.8.4 var: hoisting (partial early activation)
12.9 Closures
12.9.1 Bound variables vs. free variables
12.9.2 What is a closure?
12.9.3 Example: A factory for incrementors
12.9.4 Use cases for closures
12.10 Further reading
These are JavaScript’s main ways of declaring variables:
Before ES6, there was also var. But it has several quirks, so it’s best
to avoid it in modern JavaScript. You can read more about it in
Speaking JavaScript.
12.1 let
let i;
i = 0;
i = i + 1;
assert.equal(i, 1);
let i = 0;
12.2 const
assert.throws(
() => { i = i + 1 },
{
name: 'TypeError',
message: 'Assignment to constant variable.',
}
);
You can use const with for-of loops, where a fresh binding is created
for each iteration:
Exercise: const
exercises/variables-assignment/const_exrc.mjs
12.4 The scope of a variable
The scope of a variable is the region of a program where it can be
accessed. Consider the following code.
{ // // 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 their scopes are always the innermost surrounding blocks.
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()?
You can, however, nest a block and use the same variable name x
that you used outside the block:
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 access the old value again.
Quiz: basic
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)
}
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 everywhere. There are 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'
Global scope
globalThis is new
window.encodeURIComponent(str); // no
encodeURIComponent(str); // yes
Therefore, there are relatively few use cases for globalThis – for
example:
{
console.log(x); // What happens here?
const x;
}
The time between entering the scope of a variable and executing its
declaration is called the temporal dead zone (TDZ) of that variable:
The next example shows that the temporal dead zone is truly
temporal (related to time):
assert.equal(foo(), 123); // OK
function foo() { return 123; }
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 function declarations because I like their
syntax.
assert.throws(
() => new MyClass(),
ReferenceError);
class MyClass {}
varis an older way of declaring variables that predates const and let
(which are preferred now). Consider the following var declaration.
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);
}
12.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:
function func(x) {
const y = 123;
console.log(z);
}
function funcFactory(value) {
return () => {
return 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]);
Quiz: advanced
null number
Array Function
string
Map RegExp
symbol
Set Date
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.
let x = 123;
let y = x;
assert.equal(y, 123);
13.4.2 Objects
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 §28.2.1 “Object literals: properties”.
Array literal:
The Array literal starts and ends with square brackets []. It
creates an Array with two elements: 'foo' and 'bar'. For more
information on Array literals, consult §31.2.1 “Creating, reading,
writing Arrays”.
By default, you can freely change, add, and remove the properties of
objects:
Now the old value { prop: 'value' } of obj is garbage (not used
anymore). JavaScript will automatically garbage-collect it (remove it
from memory), at some point in time (possibly never if there is
enough free memory).
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 different objects with the same content.
13.5.1 typeof
Boolean 'boolean'
Number 'number'
x typeof x
String 'string'
Symbol 'symbol'
Function 'function'
exercises/values/typeof_exrc.mjs
Bonus: exercises/values/is_object_test.mjs
13.5.2 instanceof
x instanceof C
For example:
> (function() {}) instanceof Function
true
> ({}) instanceof Object
true
> [] instanceof Array
true
Exercise: instanceof
exercises/values/instanceof_exrc.mjs
13.6 Classes and constructor
functions
JavaScript’s original factories for objects are constructor functions:
ordinary functions that return “instances” of themselves if you
invoke them via the new operator.
In this book, I’m using the terms constructor function and class
interchangeably.
Each primitive type (except for the spec-internal types for undefined
and null) has an associated constructor function (think class):
assert.equal(Number('123'), 123);
assert.equal((123).toString, Number.prototype.toString);
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.
> Boolean(0)
false
> Number('123')
123
> String(123)
'123'
> parseInt(123.45)
123
exercises/values/conversion_exrc.mjs
Quiz
> String([1,2,3])
'1,2,3'
> String([4,5,6])
'4,5,6'
> 4 + true
5
Number(true) is 1.
14.3 Assignment operators
14.3.1 The plain assignment operator
const x = value;
let y = value;
assert.equal(str, '<b>Hello!</b>');
14.3.3 A list of all compound assignment
operators
Arithmetic operators:
+= -= *= /= %= **=
Bitwise operators:
> '' == 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 what the === operator does:
The === operator does not consider undefined and null to be equal:
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) ···
14.4.4 Even stricter than ===: Object.is()
It is even stricter than ===. For example, it considers NaN, the error
value for computations involving numbers, to be equal to itself:
The result -1 means that .indexOf() couldn’t find its argument in the
Array.
14.5 Ordering operators
Table 3: JavaScript’s ordering
operators.
Operator name
< less than
<= Less than or equal
> Greater than
>= Greater than or equal
> 5 >= 2
true
> 'bar' < 'foo'
true
The next two subsections discuss two operators that are rarely used.
The comma operator has two operands, evaluates both of them and
returns the second one:
> void (3 + 2)
undefined
Quiz
let myVar;
assert.equal(myVar, undefined);
function func(x) {
return x;
}
assert.equal(func(), undefined);
function func() {}
assert.equal(func(), undefined);
> Object.getPrototypeOf(Object.prototype)
null
> /a/.exec('x')
null
The JSON data format does not support undefined, only null:
Truthy means “is true if coerced to boolean”. Falsy means “is false
if coerced to boolean”. Both concepts are explained properly in §16.2
“Falsy and truthy values”.
15.4 undefined and null don’t have
properties
undefined and null are the two only JavaScript values where you get
an exception if you try to read a property. To explore this
phenomenon, let’s use 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
undefined and null:
> getFoo(undefined)
TypeError: Cannot read property 'foo' of undefined
> getFoo(null)
TypeError: Cannot read property 'foo' of null
> getFoo(true)
undefined
> getFoo({})
undefined
15.5 The history of undefined and
null
Quiz
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)
Most descriptive; recommended.
x ? true : false
Uses the conditional operator (explained later in this chapter).
!!x
Uses the logical Not operator (!). This operator coerces its
operand to boolean. It is applied a second time to get a non-
negated result.
if (value) {}
undefined, null
false
0, NaN
''
All other values (including all objects) are truthy:
> Boolean('abc')
true
> Boolean([])
true
> Boolean({})
true
if (!x) {
// x is falsy
}
if (x) {
// x is truthy
} else {
// x is falsy
}
Exercise: Truthiness
exercises/booleans/truthiness_exrc.mjs
16.3 Truthiness-based existence
checks
In JavaScript, if you read something that doesn’t exist (e.g., a
missing parameter or a missing property), you usually get undefined
as a result. In these cases, an existence check amounts to comparing
a value with undefined. For example, the following code checks if
object obj has the property .prop:
if (obj.prop) {
// obj has property .prop
}
Truthiness-based existence checks have one pitfall: they are not very
precise. Consider this previous example:
if (obj.prop) {
// obj has property .prop
}
obj.prop is undefined.
obj.prop is any other falsy value (null, 0, '', etc.).
function func(x) {
if (!x) {
throw new Error('Missing parameter x');
}
// ···
}
On the minus side, there is the previously mentioned pitfall: the code
also throws errors for all other falsy values.
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');
}
16.4 Conditional operator (? :)
The conditional operator is the expression version of the if
statement. Its syntax is:
It is evaluated as follows:
Examples:
// Output:
// 'then'
16.5 Binary logical operators: And
(x && y), Or (x || y)
The operators && and || are value-preserving and short-circuiting.
What does that mean?
> 12 || 'hello'
12
> 0 || 'hello'
'hello'
For example, logical And (&&) does not evaluate its second operand if
the first one is falsy:
// Output:
// 'hello'
16.5.1 Logical And (x && y)
1. Evaluate a.
2. Is the result falsy? Return it.
3. Otherwise, evaluate b and return the result.
a && b
!a ? a : b
Examples:
1. Evaluate a.
2. Is the result truthy? Return it.
3. Otherwise, evaluate b and return the result.
In other words, the following two expressions are roughly equivalent:
a || b
a ? a : b
Examples:
Sometimes you receive a value and only want to use it if it isn’t either
null or undefined. Otherwise, you’d like to use a default value, as a
fallback. You can do that via the || operator:
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 empty Array). We fix that via the || operator.
exercises/booleans/default_via_or_exrc.mjs
16.6 Logical Not (!)
The expression !x (“Not x”) is evaluated as follows:
1. Evaluate x.
2. Is it truthy? Return false.
3. Otherwise, return true.
Examples:
> !false
true
> !true
false
> !0
true
> !123
false
> !''
true
> !'abc'
false
Quiz
98
123.45
However, there is only a single type for all numbers: they are all
doubles, 64-bit floating point numbers implemented according to the
IEEE Standard for Floating-Point Arithmetic (IEEE 754).
Note that, under the hood, most JavaScript engines are often able to
use real integers, with all associated performance and storage size
benefits.
17.2 Number literals
Let’s examine literals for numbers.
Several integer literals let you express integers with various bases:
// Binary (base 2)
assert.equal(0b11, 3);
// Octal (base 8)
assert.equal(0o10, 8);
Fractions:
> 35.0
35
> 3e2
300
> 3e-2
0.03
> 0.3e2
30
7.0.toString()
(7).toString()
7..toString()
7 .toString() // space before dot
17.3 Arithmetic operators
17.3.1 Binary arithmetic operators
-8 % 5 → -3
n ** m Exponentiation ES2016 4 ** 2 → 16
> 5 % 3
2
> -5 % 3
-2
Tbl. 6 summarizes the two operators unary plus (+) and negation
(-).
> +'5'
5
> +'-12'
-12
> -'9'
-9
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 obj = { a: 1 };
++obj.a;
assert.equal(obj.a, 2);
const arr = [ 4 ];
arr[0]++;
assert.deepEqual(arr, [5]);
exercises/numbers-math/is_odd_test.mjs
17.4 Converting to number
These are three ways of converting values to numbers:
Number(value)
+value
parseFloat(value) (avoid; different than the other two!)
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);
How objects are converted to numbers can be configured – for
example, by overriding .valueOf():
exercises/numbers-math/parse_number_test.mjs
17.5 Error values
Two number values are returned when errors happen:
NaN
Infinity
17.6 Error value: NaN
NaN is an abbreviation of “not a number”. Ironically, JavaScript
considers it to be a number:
> Number('$$$')
NaN
> Number(undefined)
NaN
> Math.log(-1)
NaN
> Math.sqrt(-1)
NaN
> NaN - 3
NaN
> 7 ** NaN
NaN
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. You have to check for each
method how it handles NaN.
17.7 Error value: Infinity
When is the error value Infinity returned?
> 5 / 0
Infinity
> -5 / 0
-Infinity
function findMinimum(numbers) {
let min = Infinity;
for (const n of numbers) {
if (n < min) min = n;
}
return min;
}
const x = Infinity;
exercises/numbers-math/find_max_test.mjs
17.8 The precision of numbers:
careful with decimal fractions
Internally, JavaScript floating point numbers are represented with
base 2 (according to the IEEE 754 standard). That means that
decimal fractions (base 10) can’t always be represented precisely:
Quiz: basic
mantissa × 10exponent
Let’s try out this representation for a few floating point numbers.
For the number 1.5, we imagine there being a point after the
mantissa. We use a negative exponent to move that point one
digit to the left:
For the number 0.25, we move the point two digits to the left:
These fractions help with understanding why there are numbers that
our encoding cannot represent:
Now we can see why 0.1 + 0.2 doesn’t produce a correct result:
internally, neither of the two operands can be represented precisely.
In this section, we’ll look at a few tools for working with these
pseudo-integers.
> Math.floor(2.1)
2
> Math.floor(2.9)
2
> Math.ceil(2.1)
3
> Math.ceil(2.9)
3
> Math.trunc(2.1)
2
> Math.trunc(2.9)
2
This is the range of integers that are safe in JavaScript (53 bits plus a
sign):
[–253–1, 253–1]
> 18014398509481984
18014398509481984
> 18014398509481985
18014398509481984
> 18014398509481986
18014398509481984
> 18014398509481987
18014398509481988
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_INTEGER), true
assert.equal(Number.isSafeInteger(Number.MAX_SAFE_INTEGER+1), fa
Exercise: Detecting safe integers
exercises/numbers-math/is_safe_integer_test.mjs
The following result is incorrect and unsafe, even though both of its
operands are safe:
> 9007199254740990 + 3
9007199254740992
> 9007199254740995 - 10
9007199254740986
For each bitwise operator, this book mentions the types of its
operands and its result. Each type is always one of the following two:
assert.equal(
b32(-1),
'11111111111111111111111111111111');
assert.equal(
b32(1),
'00000000000000000000000000000001');
assert.equal(
b32(2 ** 31),
'10000000000000000000000000000000');
The bitwise Not operator (tbl. 10) inverts each binary digit of its
operand:
> b32(~0b100)
'11111111111111111111111111111011'
/**
* 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
17.13 Quick reference: numbers
17.13.1 Global functions for numbers
isFinite()
isNaN()
parseFloat()
parseInt()
Approximately: 2.2204460492503130808472633361816 ×
10-16
> Number.isFinite(Infinity)
false
> Number.isFinite(-Infinity)
false
> Number.isFinite(NaN)
false
> Number.isFinite(123)
true
> 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
> Number.isNaN(NaN)
true
> Number.isNaN(123)
false
> Number.isNaN('abc')
false
> 0.003.toString()
'0.003'
> 0.003.toExponential()
'3e-3'
> 1234..toPrecision(4)
'1234'
> 1234..toPrecision(5)
'1234.0'
> 1.234.toPrecision(3)
'1.23'
> 123.456.toString()
'123.456'
If you want the numeral to have a different base, you can specify
it via radix:
17.13.5 Sources
Wikipedia
TypeScript’s built-in typings
MDN web docs for JavaScript
ECMAScript language specification
Quiz: advanced
> Math.cbrt(8)
2
> Math.exp(0)
1
> Math.exp(1) === Math.E
true
> Math.log(1)
0
> Math.log(Math.E)
1
> Math.log(Math.E ** 2)
2
> Math.log10(1)
0
> Math.log10(10)
1
> Math.log10(100)
2
> Math.log2(1)
0
> Math.log2(2)
1
> Math.log2(4)
2
> Math.sqrt(9)
3
18.3 Rounding
Rounding means converting an arbitrary number to an integer (a
number without a decimal fraction). The following functions
implement different approaches to rounding.
> Math.ceil(2.1)
3
> Math.ceil(2.9)
3
> Math.floor(2.1)
2
> Math.floor(2.9)
2
> Math.round(2.4)
2
> Math.round(2.5)
3
Math.trunc(x: number): number [ES6]
> Math.trunc(2.1)
2
> Math.trunc(2.9)
2
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);
> Math.acos(0)
1.5707963267948966
> Math.acos(1)
0
> Math.asin(0)
0
> Math.asin(1)
1.5707963267948966
Math.asinh(x: number): number [ES6]
> Math.cos(0)
1
> Math.cos(Math.PI)
-1
> Math.sin(0)
0
> Math.sin(Math.PI / 2)
1
> Math.tan(0)
0
> Math.tan(1)
1.5574077246549023
> Math.abs(3)
3
> Math.abs(-3)
3
> Math.abs(0)
0
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
function getRandomInteger(max) {
return Math.floor(Math.random() * max);
}
> Math.sign(-8)
-1
> Math.sign(0)
0
> Math.sign(3)
1
18.6 Sources
Wikipedia
TypeScript’s built-in typings
MDN web docs for JavaScript
ECMAScript language specification
19 Unicode – a brief
introduction (advanced)
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 extended to 21 bits. These 21 bits are partitioned in 17
planes, with 16 bits each:
> '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.
19.1.2 Encoding Unicode code points:
UTF-32, UTF-16, UTF-8
UTF-32 uses 32 bits to store code units, resulting in one code unit
per code point. This format is the only one with fixed-length
encoding; all others use a varying number of code units to encode a
single code point.
UTF-8 has 8-bit code units. It uses 1–4 code units to encode a code
point:
Code
Code units
points
0000–007F 0bbbbbbb (7 bits)
Code
Code units
points
0080–07FF 110bbbbb, 10bbbbbb (5+6 bits)
0800–FFFF 1110bbbb, 10bbbbbb, 10bbbbbb (4+6+6 bits)
10000– 11110bbb, 10bbbbbb, 10bbbbbb, 10bbbbbb
1FFFFF (3+6+6+6 bits)
Notes:
Three examples:
Code
Character Code units
point
A 0x0041 01000001
π 0x03C0 11001111, 10000000
🙂 0x1F642 11110000, 10011111, 10011001,
10000010
19.2 Encodings used in web
development: UTF-16 and UTF-8
The Unicode encoding formats that are used in web development
are: UTF-16 and UTF-8.
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
···
Flag emojis are also grapheme clusters and composed of two code
point characters – for example, the flag of Japan:
Quiz
String interpolation
Multiple lines
Raw string literals (backslash has no special meaning)
20.1.1 Escaping
The backslash also lets you use the delimiter of a string literal inside
that literal:
assert.equal(
'She said: "Let\'s go!"',
"She said: \"Let's go!\"");
20.2 Accessing characters and
code points
20.2.1 Accessing JavaScript characters
This is how you iterate over the code point characters of a string via
for-of:
And this is how you convert a string into an Array of code point
characters via spreading:
exercises/strings/concat_string_array_test.mjs
20.4 Converting to string
These are three ways of converting a value x to a string:
String(x)
''+x
x.toString() (does not work for undefined and null)
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.
> String([true])
'true'
> String(['true'])
'true'
> String(true)
'true'
assert.equal(String(obj), 'hello');
Tip: The third parameter lets you switch on multiline output and
specify how much to indent – for example:
{
"first": "Jane",
"last": "Doe"
}
20.5 Comparing strings
Strings can be compared via the following operators:
Numeric Encoded
Entity Size
representation via
Grapheme 1+ code
cluster points
Unicode Code point 21 1–2 code
character bits units
JavaScript UTF-16 code unit 16 –
character bits
> '\u{1F642}'
'🙂 '
> String.fromCodePoint(0x1F642)
'🙂 '
> '🙂'.codePointAt(0).toString(16)
'1f642'
You can iterate over a string, which visits Unicode characters (not
JavaScript characters). Iteration is described later in this book. One
way of iterating is via a for-of loop:
// Output:
// '🙂'
// 'a'
> [...'🙂a']
[ '🙂', 'a' ]
> [...'🙂a'].length
2
> '🙂a'.length
3
To specify a code unit hexadecimally, you can use a code unit escape:
> '\uD83D\uDE42'
'🙂 '
> '🙂'.charCodeAt(0).toString(16)
'd83d'
When working with text that may be written in any human language,
it’s best to split at the boundaries of grapheme clusters, not at the
boundaries of Unicode characters.
Until that proposal becomes a standard, you can use one of several
libraries that are available (do a web search for “JavaScript
grapheme”).
20.7 Quick reference: Strings
Strings are immutable; none of the string methods ever modify their
strings.
> 'foo.txt'.endsWith('.txt')
true
> 'abcde'.endsWith('cd', 4)
true
> 'abc'.includes('b')
true
> 'abc'.includes('b', 2)
false
> 'abab'.indexOf('a')
0
> 'abab'.indexOf('a', 1)
2
> 'abab'.indexOf('c')
-1
> 'abab'.lastIndexOf('ab', 2)
2
> 'abab'.lastIndexOf('ab', 1)
0
> 'abab'.lastIndexOf('ab')
2
[1 of 2] .match(regExp: string | RegExp): RegExpMatchArray |
null [ES3]
Examples:
> 'ababb'.match(/a(b+)/)
{ 0: 'ab', 1: 'b', index: 0, input: 'ababb', groups: undefin
> 'ababb'.match(/a(?<foo>b+)/)
{ 0: 'ab', 1: 'b', index: 0, input: 'ababb', groups: { foo:
> 'abab'.match(/x/)
null
> 'ababb'.match(/a(b+)/g)
[ 'ab', 'abb' ]
> 'ababb'.match(/a(?<foo>b+)/g)
[ 'ab', 'abb' ]
> 'abab'.match(/x/g)
null
> 'a2b'.search(/[0-9]/)
1
> 'a2b'.search('[0-9]')
1
> '.gitignore'.startsWith('.')
true
> 'abcde'.startsWith('bc', 1)
true
> 'abc'.slice(1, 3)
'bc'
> 'abc'.slice(1)
'bc'
> 'abc'.slice(-2)
'bc'
> '🙂X🙂'.split('')
[ '\uD83D', '\uDE42', 'X', '\uD83D', '\uDE42' ]
> [...'🙂X🙂']
[ '🙂', 'X', '🙂' ]
> '#'.padStart(2)
' #'
> 'abc'.padStart(2)
'abc'
> '#'.padStart(5, 'abc')
'abca#'
> '*'.repeat()
''
> '*'.repeat(3)
'***'
$$: becomes $
$n: becomes the capture of numbered group n (alas, $0
stands for the string '$0', it does not refer to the complete
match)
$&: becomes the complete match
$`: becomes everything before the match
$': becomes everything after the match
Examples:
Example:
assert.equal(
'a 2020-04 b'.replace(
/(?<year>[0-9]{4})-(?<month>[0-9]{2})/, '|$<month>|'),
'a |04| b');
> '-a2b-'.toUpperCase()
'-A2B-'
> 'αβγ'.toUpperCase()
'ΑΒΓ'
> '-A2B-'.toLowerCase()
'-a2b-'
> 'ΑΒΓ'.toLowerCase()
'αβγ'
20.7.8 Sources
exercises/strings/remove_extension_test.mjs
Quiz
Before we dig into the two features template literal and tagged
template, let’s first examine the multiple meanings of the term
template.
21.1 Disambiguation: “template”
The following three things are significantly different despite all
having template in their names and despite all of them looking
similar:
<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:
const num = 5;
assert.equal(`Count: ${num}!`, 'Count: 5!');
Syntactically, a tagged template is a template literal that follows
a function (or rather, an expression that evaluates to a function).
That leads to the function being called. Its arguments are
derived from the contents of the template literal.
Note that getArgs() receives both the text of the literal and the
data interpolated via ${}.
21.2 Template literals
A template literal has two new features compared to a normal string
literal.
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 from the dynamic parts (the substitutions).
The library graphql-tag lets you create GraphQL queries via tagged
templates:
assert.equal(String.raw`\back`, '\\back');
Raw string literals are also useful for specifying Windows filename
paths:
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 beginning and the return plus two spaces at the end.
Output:
#
····<div>#
······Hello!#
····</div>#
··
There are two ways to fix this: via a tagged template or by trimming
the result of the template literal.
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 very end. One such template tag
is dedent by Desmond Brand:
Output:
<div>#
Hello!#
</div>
function divDedented(text) {
return `
<div>
${text}
</div>
`.trim().replace(/\n/g, '#\n');
}
console.log('Output:');
console.log(divDedented('Hello!'));
Output:
<div>#
Hello!#
</div>
21.7 Simple templating via
template literals
While template literals look like text templates, it is not immediately
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 solution is to use a template literal in the body of a
function whose parameter receives the templating data – for
example:
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 string with a table.
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 removes unnecessary whitespace.
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>
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>');
Quiz
Symbols are primitive values that are created via the factory function
Symbol():
On one hand, symbols are like objects in that each value created by
Symbol() is unique and not compared by value:
On the other hand, they also behave like primitive values. They have
to be categorized via typeof:
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);
const pt = {
x: 7,
y: 4,
toString() {
return `(${this.x}, ${this.y})`;
},
};
assert.equal(String(pt), '(7, 4)');
Properties .x and .y exist at the base level. They hold the coordinates
of the point represented by pt and are used to solve a problem –
computing with points. Method .toString() exists at a meta-level. It
is used by JavaScript to convert this object to a string.
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
22.3 Converting symbols
What happens if we convert a symbol sym to another primitive type?
Tbl. 14 has the answers.
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. Second, converting a symbol to a string is
indeed useful for diagnostic output. But it also makes sense to warn
about accidentally turning a symbol into a string (which is a different
kind of property key):
Quiz
if statement (ES1)
switch statement (ES3)
while loop (ES1)
do-while loop (ES3)
for loop (ES1)
for-of loop (ES6)
for-await-of loop (ES2018)
for-in loop (ES1)
Before we get to the actual control flow statements, let’s take a look
at two operators for controlling loops.
23.1 Conditions of control flow
statements
if, while, and do-while have conditions that are, in principle,
boolean. However, a condition only has to be truthy (true if coerced
to boolean) in order to be accepted. In other words, the following two
control flow statements are equivalent:
if (value) {}
if (Boolean(value) === true) {}
undefined, null
false
0, NaN
''
All other values are truthy. For more information, see §16.2 “Falsy
and truthy values”.
23.2 Controlling loops: break and
continue
The two operators break and continue can be used to control loops
and other statements while you are inside them.
23.2.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'
23.2.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'
23.3 if statements
These are two simple if statements: one with just a “then” branch
and one with both a “then” branch and an “else” branch:
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 statement. That statement must be terminated with a semicolon:
That means that else if is not its own construct; it’s simply an if
statement whose else_statement is another if statement.
23.4 switch statements
A switch statement looks as follows:
switch («switch_expression») {
«switch_body»
}
case «case_expression»:
«statements»
default:
«statements»
At the end of a case clause, execution continues with the next case
clause, unless you return or break – for example:
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');
That is, our implementation of dayOfTheWeek() only worked because
we used return. We can fix englishToFrench() by using break:
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);
23.4.4 Checking for illegal values via a
default clause
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
23.5 while loops
A while loop has the following syntax:
while («condition») {
«statements»
}
while (true) {
if (Math.random() === 0) break;
}
23.6 do-while loops
The do-while loop works much like while, but it checks its condition
after each loop iteration, not before.
let input;
do {
input = prompt('Enter text:');
console.log(input);
} while (input !== ':q');
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:
for (let i=0; i<3; i++) {
console.log(i);
}
// Output:
// 0
// 1
// 2
// Output:
// 'a'
// 'b'
// 'c'
If you omit all three parts of the head, you get an infinite loop:
for (;;) {
if (Math.random() === 0) break;
}
23.8 for-of loops
A for-of loop iterates over an iterable – a data container that
supports the iteration protocol. Each iterated value is stored in a
variable, as specified in the head:
But you can also use a (mutable) variable that already exists:
Note that in for-of loops you can use const. The iteration variable
can still be different for each iteration (it just can’t change during the
iteration). Think of it as a new const declaration being executed each
time in a fresh scope.
In contrast, in for loops you must declare variables via let or var if
their values change.
As mentioned before, for-of works with any iterable object, not just
with Arrays – for example, with Sets:
Lastly, you can also use for-of to iterate over the [index, element]
entries of Arrays:
Exercise: for-of
exercises/control-flow/array_to_string_test.mjs
23.9 for-await-of loops
for-await-of is like for-of, but it works with asynchronous iterables
instead of synchronous ones. And it can only be used inside async
functions and async generators.
function getOwnPropertyNames(obj) {
const result = [];
for (const key in obj) {
if ({}.hasOwnProperty.call(obj, key)) { // (A)
result.push(key);
}
}
return result;
}
assert.deepEqual(
getOwnPropertyNames({ a: 1, b:2 }),
['a', 'b']);
assert.deepEqual(
getOwnPropertyNames(['a', 'b']),
['0', '1']); // strings!
function getOwnPropertyNames(obj) {
const result = [];
for (const key of Object.keys(obj)) {
result.push(key);
}
return result;
}
Quiz
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 skip the current file and move on to the next one.
Therefore:
In line B, we use a throw statement to indicate that there was a
problem.
In line A, we use a try-catch statement to handle the problem.
readProfiles(···)
for (const filePath of filePaths)
try
readOneProfile(···)
openFile(···)
if (!fs.existsSync(filePath))
throw
throw «value»;
Any value can be thrown, but it’s best to throw an instance of Error
or its subclasses.
try {
«try_statements»
} catch (error) {
«catch_statements»
} finally {
«finally_statements»
}
try-catch
try-finally
try-catch-finally
Since ECMAScript 2019, you can omit the catch parameter (error),
if you are not interested in the value that was thrown.
The try block can be considered the body of the statement. This is
where we execute the regular code.
The following code demonstrates that the value that is thrown in line
A is indeed caught in line B.
try {
func();
} catch (err) { // (B)
assert.equal(err, errorObject);
}
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: You have created a
resource and want to always destroy it when you are done with it, no
matter what happens while working with it. You’d implement that as
follows:
assert.equal(err.message, 'Hello!');
.stack:contains a stack trace. It is supported by all mainstream
browsers.
assert.equal(
err.stack,
`
Error: Hello!
at ch_exception-handling.mjs:1:13
`.trim());
exercises/exception-handling/call_function_test.mjs
Quiz
The next two sections explain what all of those things mean.
25.2 Ordinary functions
The following code shows three ways of doing (roughly) the same
thing: creating an ordinary function.
function add(x, y) {
return x + y;
}
In JavaScript, we distinguish:
Arrow ✔ (lexical ✘
function this)
Class ✘ ✘ ✔
It’s important to note that arrow functions, methods, and classes are
still categorized as functions:
function funcDecl(x, y) {
return x * y;
}
const arrowFunc = (x, y) => {
return x * y;
};
Next, we’ll first look at the syntax of arrow functions and then how
they help with this.
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.
const id = x => x;
Ordinary functions can be both methods and real functions. Alas, the
two roles are in conflict:
As each ordinary function can be a method, it has its own this.
The own this makes it impossible to access the this of the
surrounding scope from inside an ordinary function. And that is
inconvenient for real functions.
const person = {
name: 'Jill',
someMethod() {
const ordinaryFunc = function () {
assert.throws(
() => this.name, // (A)
/^TypeError: Cannot read property 'name' of undefined$/)
};
const arrowFunc = () => {
assert.equal(this.name, 'Jill'); // (B)
};
ordinaryFunc();
arrowFunc();
},
}
If you don’t, JavaScript thinks, the arrow function has a block body
(that doesn’t return anything):
So far, all (real) functions and methods, that we have seen, were:
Single-result
Synchronous
Table 16: Syntax for creating functions and methods. The last
column specifies how many values are produced by an entity.
Result Values
Sync function Sync method
function f() {} { m() {} } value 1
f = function () {}
f = () => {}
function func() {
return 123;
}
assert.equal(func(), 123);
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);
25.6 Parameter handling
Once again, I am only mentioning functions in this section, but
everything also applies to methods.
The term parameter and the term argument basically mean the
same thing. If you want to, you can make the following distinction:
Arguments are part of a function call. They are also called actual
parameters and actual arguments.
// Output:
// 'a'
// 'b'
JavaScript uses the term callback broadly
For example:
function foo(x, y) {
return [x, y];
}
assert.deepEqual(
f(undefined, undefined),
[undefined, 0]);
function createPoint(x, y) {
return {x, y};
// same as {x: x, y: y}
}
function createPoint(...args) {
if (args.length !== 2) {
throw new Error('Please provide exactly 2 arguments!');
}
const [x, y] = args; // (A)
return {x, y};
}
The order of the arguments doesn’t matter (as long as the names
are correct).
> selectEntries({})
{ start: 0, end: -1, step: 1 }
But it does not work if you call the function without any parameters:
> selectEntries()
TypeError: Cannot destructure property `start` of 'undefined' or
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 definitions: if the parameter is missing, the default is
used.
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 is expanded into multiple arguments
– for example:
function func(x, y) {
console.log(x);
console.log(y);
}
const someIterable = ['a', 'b'];
func(...someIterable);
// same as func('a', 'b')
// Output:
// 'a'
// 'b'
Spreading and rest parameters use the same syntax (...), but they
serve opposite purposes:
arr1.push(...arr2);
assert.deepEqual(arr1, ['a', 'b', 'c', 'd']);
Positional parameters:
exercises/callables/positional_parameters_test.mjs
Named parameters:
exercises/callables/named_parameters_test.mjs
25.7 Dynamically evaluating code:
eval(), new Function() (advanced)
25.7.1 eval()
“Not via a function call” means “anything that looks different than
eval(···)”:
eval.call(undefined, '···')
(0, eval)('···') (uses the comma operator)
globalThis.eval('···')
const e = eval; e('···')
Etc.
The following code illustrates the difference:
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
Function(), then via a function expression:
25.7.3 Recommendations
Quiz
Recursion
Nested scopes
Closures
function f(x) {
return x * 2;
}
function g(y) {
const tmp = y + 1;
return f(tmp);
}
assert.equal(g(3), 8);
For each function call, you need fresh storage space for the variables
(parameters and local variables) of the called function. This is
managed via a stack of so-called execution contexts, which are
references to environments (for the purpose of this chapter).
Environments themselves are stored on the heap. That is necessary
because they occasionally live on after execution has left their scopes
(we’ll see that when exploring closures). Therefore, they themselves
can’t be managed via a stack.
function f(x) {
// Pause 3
return x * 2;
}
function g(y) {
const tmp = y + 1;
// Pause 2
return f(tmp);
}
// Pause 1
assert.equal(g(3), 8);
0 f function (x) { … }
g function (y) { … }
0 f function (x) { … }
1 g function (y) { … }
y 3
tmp 4
0 f function (x) { … }
1 g function (y) { … }
2
y 3
tmp 4
x 4
function f(x) {
function square() {
const result = x * x;
return result;
}
return square();
}
assert.equal(f(6), 36);
Here, we have three nested scopes: The top-level scope, the scope of
f(), and the scope of square(). Observations:
When you make a function call, you create a new environment. The
outer environment of that environment is the environment in which
the function was created. To help set up the field outer of
environments created via function calls, each function has an
internal property named [[Scope]] that points to its “birth
environment”.
These are the pauses we are making while executing the code:
function f(x) {
function square() {
const result = x * x;
// Pause 3
return result;
}
// Pause 2
return square();
}
// Pause 1
assert.equal(f(6), 36);
0 f [[Scope]]
Figure 10: Nested scopes, pause 1 – before calling f(): The top-level
environment has a single entry, for f(). The birth environment of f()
is the top-level environment. Therefore, f’s [[Scope]] points to it.
Execution contexts Lexical environments Functions
0 f [[Scope]]
1
outer
f(6)
x 6
square [[Scope]]
Figure 11: Nested scopes, pause 2 – while executing f(): There is now
an environment for the function call f(6). The outer environment of
that environment is the birth environment of f() (the top-level
environment at index 0). We can see that the field outer was set to
the value of f’s [[Scope]]. Furthermore, the [[Scope]] of the new
function square() is the environment that was just created.
Execution contexts Lexical environments Functions
0 f [[Scope]]
1
2 outer
f(6)
x 6
square [[Scope]]
outer
square()
result 36
function add(x) {
return (y) => { // (A)
return x + y;
};
}
assert.equal(add(3)(1), 4); // (B)
This nested way of calling add() has an advantage: if you only make
the first function call, you get a version of add() whose parameter x is
already filled in:
function add(x) {
return (y) => {
// Pause 3: plus2(5)
return x + y;
}; // Pause 1: add(2)
}
const plus2 = add(2);
// Pause 2
assert.equal(plus2(5), 7);
0 add [[Scope]]
1 plus2 ( uninit.)
outer add(2)
x 2 [[Scope]]
0 add [[Scope]]
plus2
outer add(2)
Kept alive by closure
x 2 [[Scope]]
0 add [[Scope]]
1 plus2
outer add(2)
x 2 [[Scope]]
outer plus2(5)
y 5
// Default exports
export default function f() {} // declaration with optional name
// Replacement for `const` (there must be exactly one value)
export default 123;
27.1.2 Importing
// Named imports
import {foo, bar as b} from './some-module.mjs';
// Namespace import
import * as someModule from './some-module.mjs';
// Default import
import someModule from './some-module.mjs';
// Combinations:
import someModule, * as someModule from './some-module.mjs';
import someModule, {foo, bar as b} from './some-module.mjs';
<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();
}
The original CommonJS standard for modules was created for server
and desktop platforms. It was the foundation of the original Node.js
module system, where it achieved enormous popularity.
Contributing to that 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
standard (which has a few additional features). This is an example of
a 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,
};
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,
};
});
function internalFunc() {
···
}
lib/my-math.mjs
main.mjs
exercises/modules/export_named_test.mjs
assert.deepEqual(
Object.keys(myMath), ['LIGHTSPEED', 'square']);
The named export style we have seen so far was inline: We exported
entities by prefixing them with the keyword export.
But we can also use separate export clauses. For example, this is
what lib/my-math.mjs looks like with an export clause:
function times(a, b) {
return a * b;
}
function square(x) {
return times(x, x);
}
const LIGHTSPEED = 299792458;
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
};
27.7 Default exports and imports
Each module can have at most one default export. The idea is that
the module is the default-exported value.
A module can have both named exports and a default export, but
it’s usually better to stick to one export style per module.
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 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 following hypothetical code:
With this code, you don’t know which one of the three values is the
default export.
export {
greet as default,
};
const obj = {
default: 123,
};
assert.equal(obj.default, 123);
27.8 More details on exporting and
importing
27.8.1 Imports are read-only views on
exports
counter.mjs
main.mjs
{
"name": "foo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
main: specifies the module that “is” the package (explained later
in this chapter).
scripts: are commands that you can execute via npm run. For
example, the script test can be executed via npm run test.
/tmp/a/b/node_modules
/tmp/a/node_modules
/tmp/node_modules
When installing a package foo, npm uses the closest node_modules. If,
for example, we are inside /tmp/a/b/ and there is a node_modules in
that directory, then npm puts the package inside the directory:
/tmp/a/b/node_modules/foo/
// /home/jane/proj/main.mjs
import * as theModule from 'the-package/the-module.mjs';
/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
./my-module.mjs
./some-func.mjs
But that style does not work for default imports: I like underscore-
casing for namespace objects, but it is not a good choice for
functions, etc.
27.11 Module specifiers
Module specifiers are the strings that identify modules. They work
slightly differently in browsers and Node.js. Before we can look at the
differences, we need to learn about the different categories of module
specifiers.
'./some/other/module.mjs'
'../../lib/counter.mjs'
'/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 of a single filename without an extension. Examples:
'lodash'
'the-package'
Deep import path: starts with a bare path and has at least one
slash. Example:
'the-package/dist/the-module.mjs'
You may have to switch it on via a command line flag. See the
Node.js documentation for details.
Node.js handles module specifiers as follows:
All specifiers, except bare paths, must refer to actual files. That is,
ESM does not support the following CommonJS features:
assert.equal(
path.join('a/b/c', '../d'), 'a/b/d');
You must use it at the top level of a module. That is, you can’t,
for example, import something when you are inside a block.
The module specifier is always fixed. That is, you can’t change
what you import depending on a condition. And you can’t
assemble a specifier dynamically.
lib/my-math.mjs
main1.mjs
main2.mjs
function loadConstant() {
return import(moduleSpecifier)
.then(myMath => {
const result = myMath.LIGHTSPEED;
assert.equal(result, 299792458);
return result;
});
}
if (isLegacyPlatform()) {
import('./my-polyfill.mjs')
.then(···);
}
import(`messages_${getLocale()}.mjs`)
.then(···);
27.13 Preview: import.meta.url
“import.meta” is an ECMAScript feature proposed by Domenic
Denicola. The object import.meta holds metadata for the current
module.
'https://example.com/code/main.mjs'
'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:
If you need a path that can be used in the local file system, then
property .pathname of URL instances does not always work:
assert.equal(
new URL('file:///tmp/with%20space.txt').pathname,
'/tmp/with%20space.txt');
Every time our web applications starts, it must first execute all
polyfills for features that may not be available everywhere.
Afterwards, we can be sure that those features are available natively.
Quiz
SuperClass
superData
superMthd
mthd ƒ
MyClass SubClass
mthd ƒ __proto__ data subData
data 4 data 4 mthd subMthd
First, we’ll explore objects-as-records. Even though property keys are strings
or symbols under the hood, they will appear as fixed identifiers to us, in this
part of the chapter.
Later, we’ll explore objects-as-dictionaries. Note that Maps are usually better
dictionaries than objects. However, some of the operations that we’ll
encounter, can also be useful for objects-as-records.
28.2 Objects as records
Let’s first explore the role record of objects.
Object literals are one way of creating objects-as-records. They are a stand-out
feature of JavaScript: you can directly create objects – no need for classes! This is
an example:
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, you can use first_name as a property key, but not first-name).
However, reserved words are allowed:
const obj = {
if: true,
const: true,
};
function createPoint(x, y) {
return {x, y};
}
assert.deepEqual(
createPoint(9, 2),
{ x: 9, y: 2 }
);
const jane = {
first: 'Jane',
last: 'Doe',
};
assert.equal(jane.unknownProperty, undefined);
const obj = {
prop: 1,
};
assert.equal(obj.prop, 1);
obj.prop = 2; // (A)
assert.equal(obj.prop, 2);
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', // data 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. That enables method
.says() to access the sibling property .first in line A.
28.2.6.1 Getters
const jane = {
first: 'Jane',
last: 'Doe',
get full() {
return `${this.first} ${this.last}`;
},
};
28.2.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/single-objects/color_point_object_test.mjs
28.3 Spreading into object literals (...)
Inside a function call, spreading (...) turns the iterated values of an iterable
object into arguments.
Inside an object literal, a spread property adds the properties of another object
to the current one:
> {...undefined}
{}
> {...null}
{}
> {...123}
{}
> {...'abc'}
{ '0': 'a', '1': 'b', '2': 'c' }
> {...['a', 'b']}
{ '0': 'a', '1': 'b' }
Property .length of strings and of Arrays is hidden from this kind of operation (it
is not enumerable; see §28.7.3 “Property attributes and property descriptors” for
more information).
The first level of copy is really a copy: If you change any properties at that level, it
does not affect the original:
copy.a = 2;
assert.deepEqual(
original, { a: 1, b: {foo: 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.foo = false;
assert.deepEqual(
original, { a: 1, b: {foo: false} });
Deep copies of objects (where all levels are copied) are notoriously difficult to
do generically. Therefore, JavaScript does not have a built-in operation for
them (for now). If you need such an operation, you have to implement it
yourself.
If one of the inputs of your code is an object with data, you can make properties
optional by specifying default values that are used if those 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:
const DEFAULTS = {foo: 'a', bar: 'b'};
const providedData = {foo: 1};
The result, the object allData, is created by copying DEFAULTS and overriding its
properties with those of providedData.
But you don’t need an object to specify the default values; you can also specify
them inside the object literal, individually:
exercises/single-objects/update_name_test.mjs
28.4 Methods
28.4.1 Methods are properties whose values are
functions
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
},
};
Why is that? We learned in the chapter on callable values, that ordinary functions
play several roles. Method is one of those roles. Therefore, under the hood, jane
roughly looks as follows.
const jane = {
first: 'Jane',
says: function (text) {
return `${this.first} says “${text}”`;
},
};
Remember that each function someFunc is also an object and therefore has
methods. One such method is .call() – it lets you call a function while specifying
this via a parameter:
const obj = {
method(x) {
assert.equal(this, obj); // implicit parameter
assert.equal(x, 'a');
},
};
obj.method.call(obj, 'a');
As an aside, that means that there are actually two different dot operators:
They are different in that (2) is not just (1) followed by the function call operator
(). Instead, (2) additionally specifies a value for this.
function func(x) {
assert.equal(this, undefined); // implicit parameter
assert.equal(x, 'a');
}
func('a');
func.call(undefined, 'a');
this being set to undefined during a function call, indicates that it is a feature that
is only needed during a method call.
Next, we’ll examine the pitfalls of using this. Before we can do that, we need one
more tool: method .bind() of functions.
boundFunc('a', 'b')
someFunc.call(thisValue, arg1, arg2, 'a', 'b')
In the following example, we create add8(), a function that has one parameter, by
binding the first parameter of add() to 8.
function add(x, y) {
return x + y;
}
In the following code, we turn method .says() into the stand-alone function
func():
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`; // (A)
},
};
Setting this to jane via .bind() is crucial here. Otherwise, func() wouldn’t work
properly because this is used in line A.
We now know quite a bit about functions and methods and are ready to take a
look at the biggest pitfall involving methods and this: function-calling a method
extracted from an object can fail if you are not careful.
assert.throws(
() => jane.says.call(undefined, 'hello'), // `this` is undefined!
{
name: 'TypeError',
The .bind() ensures that this is always jane when we call func().
The following is a simplified version of code that you may see in actual web
development:
class ClickHandler {
constructor(id, elem) {
this.id = id;
elem.addEventListener('click', this.handleClick); // (A)
}
handleClick(event) {
alert('Clicked ' + this.id);
}
}
elem.addEventListener('click', this.handleClick.bind(this));
exercises/single-objects/method_extraction_exrc.mjs
Consider the following problem: when you are inside an ordinary function, you
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 variable in an
outer scope. That is called shadowing. The following code is an example:
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x; // (A)
});
},
};
assert.throws(
() => prefixer.prefixStringArray(['a', 'b']),
/^TypeError: Cannot read property 'prefix' of undefined$/);
The simplest way to fix this problem is via an arrow function, which doesn’t have
its own this and therefore doesn’t shadow anything:
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 invoking
the callback:
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
},
this); // (A)
},
1. Extracting methods
2. Accidentally shadowing this
“Avoid the keyword function”: Never use ordinary functions, only arrow
functions (for real functions) and method definitions.
It prevents the second pitfall because ordinary functions are never used as
real functions.
this becomes easier to understand because it will only appear inside
methods (never inside ordinary functions). That makes it clear that this is
an OOP feature.
Alas, there is no simple way around the first pitfall: whenever you extract a
method, you have to be careful and do it properly – for example, by binding this.
Function call:
Ordinary functions: this === undefined (in strict mode)
Arrow functions: this is same as in surrounding scope (lexical this)
Method call: this is receiver of call
new: this refers to newly created instance
However, I like to pretend that you can’t access this in top-level scopes because
top-level this is confusing and rarely useful.
28.5 Objects as dictionaries (advanced)
Objects work best as records. 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: 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 objects-as-records. This section concludes with tips for actually using objects
as dictionaries (spoiler: use Maps if you can).
So far, we have always used objects as records. Property keys were fixed tokens
that had to be valid identifiers and internally became strings:
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 section,
we’ll use arbitrary fixed strings as keys. In the next subsection, we’ll dynamically
compute keys.
First, when creating property keys via object literals, we can quote property keys
(with single or double quotes):
const obj = {
'Can be any string!': 123,
};
Second, when getting or setting properties, we can use square brackets with
strings inside them:
// 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!';
},
};
So far, property keys were always fixed strings inside object literals. In this
section we learn how to dynamically compute property keys. That enables us to
use either arbitrary strings or symbols.
const obj = {
['Hello world!']: true,
['f'+'o'+'o']: 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 works
with arbitrary expressions:
assert.equal(obj['f'+'o'+'o'], 123);
assert.equal(obj['==> foo'.slice(-3)], 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
available for arbitrary strings and symbols.
exercises/single-objects/update_property_test.mjs
const obj = {
foo: 'abc',
bar: false,
};
assert.equal(
obj.foo ? 'exists' : 'does not exist',
'exists');
assert.equal(
obj.unknownKey ? 'exists' : 'does not exist',
'does not exist');
The previous checks work because obj.foo is truthy and because reading a
missing property returns undefined (which is falsy).
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.bar ? 'exists' : 'does not exist',
'does not exist'); // should be: 'exists'
const obj = {
foo: 123,
};
assert.deepEqual(Object.keys(obj), ['foo']);
delete obj.foo;
assert.deepEqual(Object.keys(obj), []);
Object.getOwnPropertyNames() ✔ ✔ ✔
Object.getOwnPropertySymbols() ✔ ✔ ✔
non-
enumerable string symbol
e.
Reflect.ownKeys() ✔ ✔ ✔ ✔
Each of the methods in tbl. 18 returns an Array with the own property keys of the
parameter. In the names of the methods, you can see that the following
distinction is made:
The next section describes the term enumerable and demonstrates each of the
methods.
28.5.5.1 Enumerability
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,
]);
exercises/single-objects/find_key_test.mjs
1. Properties with string keys that contain integer indices (that includes Array
indices):
In ascending numeric order
2. Remaining properties with string keys:
In the order in which they were added
3. Properties with symbol keys:
In the order in which they were added
The following example demonstrates how property keys are sorted according to
these rules:
assert.deepEqual(
Object.fromEntries([['foo',1], ['bar',2]]),
{
foo: 1,
bar: 2,
}
);
Object.fromEntries() does the opposite of Object.entries().
To demonstrate both, we’ll use them to implement two tool functions from the
library Underscore in the next subsubsections.
pickreturns a copy of object that only has those properties whose keys are
mentioned as arguments:
const address = {
street: 'Evergreen Terrace',
number: '742',
city: 'Springfield',
state: 'NT',
zip: '49007',
};
assert.deepEqual(
pick(address, 'street', 'number'),
{
street: 'Evergreen Terrace',
number: '742',
}
);
invert 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'}
);
We can implement invert() like this:
function invert(object) {
const mappedEntries = Object.entries(object)
.map(([key, value]) => [value, key]);
return Object.fromEntries(mappedEntries);
}
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/single-objects/omit_properties_test.mjs
If you use plain objects (created via object literals) as dictionaries, you have to
look out for two pitfalls.
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 it
inherits from its prototype, Object.prototype.
The second pitfall is that you can’t use the property key __proto__ because it has
special powers (it sets the prototype of the object):
dict['__proto__'] = 123;
// No property was added to dict:
assert.deepEqual(Object.keys(dict), []);
Whenever you can, use Maps. They are the best solution for dictionaries.
If you can’t, use a library for objects-as-dictionaries that does everything
safely.
If you can’t, use an object without a prototype.
dict['__proto__'] = 123;
assert.deepEqual(Object.keys(dict), ['__proto__']);
We avoided both pitfalls: First, a property without a prototype does not inherit
any properties (line A). Second, in modern JavaScript, __proto__ is implemented
via Object.prototype. That means that it is switched off if Object.prototype is not
in the prototype chain.
.toString()
.valueOf()
28.6.1 .toString()
28.6.2 .valueOf()
28.7.1 Object.assign()
This expression assigns all properties of source_1 to target, then all properties of
source_2, etc. At the end, it returns target – for example:
assert.deepEqual(
result, { foo: 1, bar: 4, baz: 3 });
// target was modified and returned:
assert.equal(result, target);
The use cases for Object.assign() are similar to those for spread properties. In a
way, it spreads destructively.
When you 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 you read the attributes of a
property obj.foo:
const obj = {
foo: 1,
bar: 2,
};
Quiz
SuperClass
superData
superMthd
mthd ƒ
MyClass SubClass
mthd ƒ __proto__ data subData
data 4 data 4 mthd subMthd
In an object literal, you can set the prototype via the special property
__proto__:
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};
proto
__proto__
protoProp 'a'
obj
__proto__
objProp 'b'
Figure 19: 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.
> Object.keys(obj)
[ 'foo' ]
Read on for another operation that also only considers own
properties: setting properties.
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};
proto
__proto__
protoProp 'a'
obj
__proto__
objProp 'b'
protoProp 'x'
Object.setPrototypeOf(obj, proto2);
assert.equal(Object.getPrototypeOf(obj), proto2);
p.isPrototypeOf(o)
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(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false);
const jane = {
name: 'Jane',
describe() {
return 'Person named '+this.name;
},
};
const tarzan = {
name: 'Tarzan',
describe() {
return 'Person named '+this.name;
},
};
We have two objects that are very similar. Both have two properties
whose names are .name and .describe. Additionally, method
.describe() is the same. How can we avoid duplicating that method?
const PersonProto = {
describe() {
return 'Person named ' + this.name;
},
};
const jane = {
__proto__: PersonProto,
name: 'Jane',
};
const tarzan = {
__proto__: PersonProto,
name: 'Tarzan',
};
The name of the prototype reflects that both jane and tarzan are
persons.
PersonProto
describe function() {···}
jane tarzan
__proto__ __proto__
name 'Jane' name 'Tarzan'
Figure 21: Objects jane and tarzan share method .describe(), via
their common prototype PersonProto.
Fig. 21 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 you 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 the method still works. tarzan.describe()
works similarly.
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');
29.2 Classes
We are now ready to take on classes, which are basically a compact
syntax for setting up prototype chains. Under the hood, JavaScript’s
classes are unconventional. But that is something you rarely see
when working with them. They should normally feel familiar to
people who have used other object-oriented programming languages.
class Person {
constructor(name) {
this.name = name;
}
describe() {
return 'Person named '+this.name;
}
}
This was a first look at classes. We’ll explore more features soon, but
first we need to learn the internals of classes.
Person Person.prototype
prototype constructor
describe function() {...}
jane
__proto__
name 'Jane'
Figure 22: The class Person has the property .prototype that points
to an object that is the prototype of all instances of Person. jane is
one such instance.
This setup also exists due to backward compatibility. But it has two
additional benefits.
assert.equal(tarzan.constructor.name, 'Person');
class Foo {
constructor(prop) {
this.prop = prop;
}
protoMethod() {
return 'protoMethod';
}
get protoGetter() {
return 'protoGetter';
}
}
> foo.protoMethod()
'protoMethod'
> foo.protoGetter
'protoGetter'
All constructs in the body of the following class declaration create so-
called static properties – properties of Bar itself.
class Bar {
static staticMethod() {
return 'staticMethod';
}
static get staticGetter() {
return 'staticGetter';
}
}
The static method and the static getter are used as follows:
> Bar.staticMethod()
'staticMethod'
> Bar.staticGetter
'staticGetter'
They help tools such as IDEs and type checkers with their work
and enable new features there.
JavaScript engines optimize them. That is, code that uses classes
is almost always faster than code that uses a custom inheritance
library.
exercises/proto-chains-classes/point_class_test.mjs
29.3 Private data for classes
This section describes techniques for hiding some of the data of an
object from the outside. We discuss them in the context of classes,
but they also work for objects created directly, e.g., via object literals.
class Countdown {
constructor(counter, action) {
this._counter = counter;
this._action = action;
}
dec() {
this._counter--;
if (this._counter === 0) {
this._action();
}
}
}
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)();
}
}
}
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);
}
}
}
Two comments:
Inside a .constructor() method, you must call the super-
constructor via super() before you can access this. That’s
because this doesn’t exist before the super-constructor is called
(this phenomenon is specific to classes).
Exercise: Subclassing
exercises/proto-chains-classes/color_point_class_test.mjs
__proto__ __proto__
prototype
Person Person.prototype
__proto__ __proto__
prototype
Employee Employee.prototype
__proto__
jane
Figure 23: These are the objects that make up class Person and its
subclass, Employee. The left column is about classes. The right
column is about the Employee instance jane and its prototype chain.
The classes Person and Employee from the previous section are made
up of several objects (fig. 23). One key insight for understanding how
these objects are related is that there are two prototype chains:
The instance prototype chain starts with jane and continues with
Employee.prototype and Person.prototype. In principle, the
prototype chain ends at this point, but we get one more object:
Object.prototype. This prototype provides services to virtually all
objects, which is why it is included here, too:
x instanceof C
C.prototype.isPrototypeOf(x)
If we go back to fig. 23, we can confirm that the prototype chain does
lead us to the following correct answers:
const p = Object.getPrototypeOf.bind(Object);
null
__proto__
Object.prototype
__proto__
{}
Fig. 24 shows a diagram for this prototype chain. We can see that {}
really is an instance of Object – Object.prototype is in its prototype
chain.
__proto__
Object.prototype
__proto__
Array.prototype
__proto__
[]
Figure 25: The prototype chain of an Array has these members: the
Array instance, Array.prototype, Object.prototype, null.
class Object {
get __proto__() {
return Object.getPrototypeOf(this);
}
set __proto__(other) {
Object.setPrototypeOf(this, other);
}
// ···
}
That means that you can switch .__proto__ off by creating an object
that doesn’t have Object.prototype in its prototype chain (see the
previous section):
> '__proto__' in {}
true
> '__proto__' in { __proto__: null }
false
Let’s examine how method calls work with classes. We are revisiting
jane from earlier:
class Person {
constructor(name) {
this.name = name;
}
describe() {
return 'Person named '+this.name;
}
}
const jane = new Person('Jane');
Person.prototype
__proto__
describe function() {···}
jane
__proto__
name 'Jane'
Figure 26: The prototype chain of jane starts with jane and continues
with Person.prototype.
Normal method calls are dispatched – the method call
jane.describe() happens in two steps:
func.call(jane);
You can make the same method call directly, without dispatching:
Person.prototype.describe.call(jane)
Direct method calls become useful when you are working with
methods of Object.prototype. For example,
Object.prototype.hasOwnProperty(k) checks if this has a non-
inherited property whose key is k:
> const obj = { foo: 123 };
> obj.hasOwnProperty('foo')
true
> obj.hasOwnProperty('bar')
false
This pattern may seem inefficient, but most engines optimize this
pattern, so performance should not be an issue.
The following code confirms that the mixin worked: Car has method
.setBrand() of Branded.
The same class can extend a single superclass and zero or more
mixins.
The same mixin can be used by multiple classes.
29.5 FAQ: objects
29.5.1 Why do objects preserve the
insertion order of properties?
Quiz
The iteration protocol connects these two groups via the interface
Iterable: data sources deliver their contents sequentially “through
it”; data consumers get their input via it.
Data consumers Interface Data sources
Iterable Maps
spreading Strings
Figure 27: Data consumers such as the for-of loop use the interface
Iterable. Data sources such as Arrays implement that interface.
Figure 28: Iteration has two main interfaces: Iterable and Iterator.
The former has a method that returns the latter.
interface Iterable<T> {
[Symbol.iterator]() : Iterator<T>;
}
interface Iterator<T> {
next() : IteratorResult<T>;
}
interface IteratorResult<T> {
value: T;
done: boolean;
}
The interfaces are used as follows:
You ask an Iterable for an iterator via the method whose key is
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:
.value is the iterated value.
.done indicates if the end of the iteration has been reached
yet. It is true after the last iterated value and false
beforehand.
30.3 Iterating manually
This is an example of using the iteration protocol:
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'
Exercise: Using sync iteration manually
exercises/sync-iteration-use/sync_iteration_manually_exrc.mjs
30.4 Iteration in practice
We have seen how to use the iteration protocol manually, and it is
relatively cumbersome. But the protocol is not meant to be used
directly – it is meant to be used via higher-level language constructs
built on top of it. This section shows what that looks like.
As does Array-destructuring:
Arrays
Strings
Maps
Sets
(Browsers: DOM data structures)
func(...iterable);
const arr = [...iterable];
yield*:
function* generatorFunction() {
yield* iterable;
}
Quiz
The Array literal starts and ends with square brackets []. It creates
an Array with three elements: 'a', 'b', and 'c'.
assert.equal(arr[0], 'a');
arr[0] = 'x';
assert.deepEqual(arr, ['x', 'b', 'c']);
Every Array has a property .length that can be used to both read and
change(!) the number of elements in an Array.
If you write to the Array at the index of the length, you append an
element:
> arr.push('d');
> arr
[ 'a', 'b', 'c', 'd' ]
If you set .length, you are pruning the Array by removing elements:
> arr.length = 1;
> arr
[ 'a' ]
To clear (empty) an Array, you can either set its .length to zero:
or you can assign a new empty Array to the variable storing the
Array:
let arr = ['a', 'b', 'c'];
arr = [];
assert.deepEqual(arr, []);
exercises/arrays/remove_empty_lines_push_test.mjs
assert.deepEqual(
Object.keys(arr),
['0', '1', 'prop']);
assert.deepEqual(
Array.from({length:2, 0:'a', 1:'b'}),
[ 'a', 'b' ]);
interface ArrayLike<T> {
length: number;
[n: number]: T;
}
Inside an Array literal, spreading via ... converts any iterable object
into a series of Array elements. For example:
.from<T, U>(
iterable: Iterable<T> | ArrayLike<T>,
mapFunc: (v: T, i: number) => U,
thisArg?: any)
: U[]
This is an example:
Note that the result has three holes (empty slots) – the last comma in
an Array literal is always ignored.
Caveat: If you use .fill() with an object, then each Array element
will refer to this object (sharing it).
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 you are
accessing them via numbers. But the square brackets operator [] for
doing so is the same operator that is used for accessing properties. It
coerces any value (that is not a symbol) to a string. Therefore, Array
elements are (almost) normal properties (line A) and it doesn’t
matter if you use numbers or strings as indices (lines B and C):
To make matters even more confusing, this is only how the language
specification defines things (the theory of JavaScript, if you will).
Most JavaScript engines optimize under the hood and do use actual
integers to access Array elements (the practice of JavaScript, if you
will).
Property keys (strings!) that are used for Array elements are called
indices. A string str is an index if converting it to a 32-bit unsigned
integer and back results in the original value. Written as a formula:
assert.deepEqual(
Object.keys(arr),
['0', '1', 'prop']);
assert.equal(arr.length, 2);
assert.deepEqual(
[...arr.keys()], [0, 1]);
assert.deepEqual(
[...arr.entries()], [[0, 'a'], [1, 'b']]);
Alas, there are many different ways in which Array operations treat
holes.
> [...['a',,'b'].keys()]
[ 0, 1, 2 ]
> Object.keys(['a',,'b'])
[ '0', '2' ]
Alas, a rest element must come last in an Array. Therefore, you can
only use it to extract suffixes.
exercises/arrays/queue_via_array_test.mjs
31.10 Methods: iteration and
transformation (.find(), .map(),
.filter(), etc.)
That is, the callback gets three parameters (it is free to ignore any of
them):
.map() fills its result with the values returned by its callback:
.find() returns the first Array element for which its callback
returns true:
.find() returns the first element for which its callback returns a
truthy value (and undefined if it can’t find anything):
.findIndex() returns the index of the first element for which its
callback returns a truthy value (and -1 if it can’t find anything):
exercises/arrays/number_lines_test.mjs
.flatMap<U>(
callback: (value: T, index: number, array: T[]) => U|Array<U>,
thisValue?: any
): U[]
The result of the Array method .map() always has the same length as
the Array it is invoked on. That is, its callback can’t skip Array
elements it isn’t interested in. The ability of .flatMap() to do so is
useful in the next example.
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:
The Array method .map() maps each input Array element to one
output element. But what if we want to map it to multiple output
elements?
That becomes necessary in the following example:
function stringsToCodePoints(strs) {
return strs.flatMap(str => [...str]);
}
Exercises: .flatMap()
exercises/arrays/convert_to_numbers_test.mjs
exercises/arrays/replace_objects_test.mjs
For example:
exercises/arrays/remove_empty_lines_filter_test.mjs
.reduce<U>(
callback: (accumulator: U, element: T, index: number, array: T
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”.
You could say that the callback folds Array elements into the
accumulator. That’s why this operation is called “fold” in functional
programming.
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
callback has already visited.
How was the result 6 derived from the Array in line A? Via the
following 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 little easier to understand – especially if you are not
familiar with functional programming.
One limitation of .reduce() is that you can’t finish early (in a for-of
loop, you can break). Here, we always immediately return the result
once we have found it.
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()
As you can see, all unaccented uppercase letters come before all
unaccented lowercase letters, which come before all accented letters.
Use Intl, the JavaScript internationalization API, if you want proper
sorting for human languages.
Note that .sort() sorts in place; it changes and returns its receiver:
You can customize the sort order via the parameter compareFunc,
which must return a number that is:
negative if a < b
zero if a === b
positive if a > b
function compareNumbers(a, b) {
if (a < b) {
return -1;
} else if (a === b) {
return 0;
} else {
return 1;
}
}
assert.deepEqual(
[200, 3, 10].sort(compareNumbers),
[3, 10, 200]);
It is cryptic.
There is a risk of numeric overflow or underflow, if a-b becomes
a large positive or negative number.
You also need to use a compare function if you want to sort objects.
As an example, the following code shows how to sort objects by age.
exercises/arrays/sort_objects_test.mjs
31.12 Quick reference: Array<T>
Legend:
assert.equal(
MyArray.of('a', 'b') instanceof MyArray, true);
The result is the index of the first element for which predicate
returns a truthy value. If there is no such element, the result is
-1.
// Output:
// 'a', 0
// 'b', 1
.lastIndexOf(searchElement: T, fromIndex=this.length-1):
number [R, ES5]
Removes and returns the last element of the receiver. That is, it
treats the end of the receiver as a stack. The opposite of .push().
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 receiver after the change. The opposite of .pop().
You can customize the sort order via compareFunc, which returns
a number that is:
negative if a < b
zero if a === b
positive if a > b
.sort() is stable
Inserts the items at the beginning of the receiver and returns its
length after this modification.
> const arr = ['c', 'd'];
> arr.unshift('e', 'f')
4
> arr
[ 'e', 'f', 'c', 'd' ]
31.12.4 Sources
Quiz
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 normal Arrays (.map(),
.filter(), etc.).
An ArrayBuffer itself is a black box: if you want to access its data, you
must wrap it in another object – a view object. Two kinds of view
objects are available:
Typed Arrays are used much like normal Arrays with a few notable
differences:
The following code shows three different ways of creating the same
Typed Array:
// Argument: Typed Array or Array-like object
const ta1 = new Uint8Array([0, 1, 2]);
assert.deepEqual(ta1, ta2);
assert.deepEqual(ta1, ta3);
assert.deepEqual(
typedArray.buffer, new ArrayBuffer(4)); // 4 bytes
Tbl. 19 lists the available element types. These types (e.g., Int32)
show up in two locations:
They are the types of the elements of Typed Arrays. For
example, all elements of a Int32Array have the type Int32. The
element type is the only aspect of Typed Arrays that differs.
They are the lenses through which an ArrayBuffer accesses its
DataView when you use methods such as .getInt32() and
.setInt32().
The highest value plus one is converted to the lowest value (0 for
unsigned integers).
The lowest value minus one is converted to the highest value.
32.2.2 Endianness
Big endian: the most significant byte comes first. For example,
the Uint16 value 0x4321 is stored as two bytes – first 0x43, then
0x21.
Little endian: the least significant byte comes first. For example,
the Uint16 value 0x4321 is stored as two bytes – first 0x21, then
0x43.
.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));
The optional mapfn lets you 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:
The static method .from() can optionally both map and convert
between Typed Array types. Less can go wrong if you use that
method.
To see why that is, let us first convert a Typed Array to a Typed Array
with a higher precision. If we use .from() to map, the result is
automatically correct. Otherwise, you must first convert and then
map.
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
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 type that you specify via its receiver.
Typed Arrays are iterable. That means that you can use the for-of
loop and other iteration-based mechanisms:
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:
ta[0] = 257;
assert.equal(ta[0], 1); // 257 % 256 (overflow)
ta[0] = '2';
assert.equal(ta[0], 2);
Typed Arrays don’t have a method .concat(), like normal Arrays do.
The workaround is to use their overloaded method .set():
The following function uses that method to copy zero or more Typed
Arrays (or Array-like objects) into an instance of resultConstructor:
Indices for the bracket operator [ ]: You can only use non-
negative indices (starting at 0).
You can’t change the length of an ArrayBuffer; you can only create a
new one with a different length.
ArrayBuffer.isView(arg: any)
assert.deepEqual(
Int16Array.from(Int8Array.of(127, 126, 125), x => x * 2),
Int16Array.of(254, 252, 250));
assert.deepEqual(
Int16Array.of(-1234, 5, 67),
new Int16Array([-1234, 5, 67]) );
32.6.2 Properties of
TypedArray<T>.prototype
Returns the offset where this Typed Array “starts” inside its
ArrayBuffer.
.subarray(startIndex=0, endIndex=this.length):
TypedArray<T>
Returns a new Typed Array that has the same buffer as this
Typed Array, but a (generally) smaller range. If startIndex is
non-negative then the first element of the resulting Typed Array
is this[startIndex], the second this[startIndex+1] (etc.). If
startIndex in negative, it is converted appropriately.
32.6.2.2 Array methods
Each Typed Array constructor has a name that follows the pattern
«ElementType»Array, where «ElementType» is one of the element types
in the table at the beginning. That means that there are nine
constructors for Typed Arrays:
Float32Array, Float64Array
Int8Array, Int16Array, Int32Array
Uint8Array, Uint8ClampedArray, Uint16Array, Uint32Array
new «ElementType»Array(length=0)
length * «ElementType»Array.BYTES_PER_ELEMENT
«ElementType»Array.BYTES_PER_ELEMENT: number
32.6.5 Properties of
«ElementType»Array.prototype
.BYTES_PER_ELEMENT: number
Float32, Float64
Int8, Int16, Int32
Uint8, Uint16, Uint32
get .buffer()
get .byteLength()
get .byteOffset()
Returns at which offset this DataView starts accessing the bytes
in its buffer.
First, you can use the constructor without any parameters to create
an empty Map:
.set() and .get() are for writing and reading values (given keys).
map.set('foo', 123);
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') || '', '');
assert.equal(map.has('foo'), true);
assert.equal(map.delete('foo'), true)
assert.equal(map.has('foo'), false)
33.1.4 Determining the size of a Map and
clearing it
assert.equal(map.size, 2)
map.clear();
assert.equal(map.size, 0)
assert.deepEqual(
[...map.entries()],
[[false, 'no'], [true, 'yes']]);
Map instances are also iterables over entries. In the following code,
we use destructuring to access the keys and values of map:
Maps record in which order entries were created and honor that
order when listing entries, keys, or values:
As long as a Map only uses strings and symbols as keys, you can
convert it to an object (via Object.fromEntries()):
You 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]]));
33.2 Example: Counting characters
countChars() returns a Map that maps characters to numbers of
occurrences.
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, you can use NaN as a key in Maps, just like any
other value:
You can .map() and .filter() an Array, but there are no such
operations for a Map. The solution is:
Mapping originalMap:
Filtering originalMap:
To combine map1 and map2, we turn them into Arrays via spreading
(...) and concatenate those Arrays. Afterward, we convert the result
back to a Map. All of that is done in line A.
33.5.1 Constructor
Both iterating and looping happen in the order in which entries were
added to a Map.
Returns an iterable with one [key, value] pair for each entry in
this Map. The pairs are Arrays of length 2.
If you need a dictionary-like data structure with keys that are neither
strings nor symbols, you have no choice: you must use a Map.
If, however, your keys are either strings or symbols, you must decide
whether or not to use an object. A rough general guideline is:
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:
You normally want Map keys to be compared by value (two keys are
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).
Quiz
They are black boxes, where a value can only be accessed if you
have both the WeakMap and the key.
The keys of a WeakMap are weakly held: if an object is a key in a
WeakMap, it can still be garbage-collected. That lets us use
WeakMaps to attach data to objects.
The next two sections examine in more detail what that means.
34.1 WeakMaps are black boxes
It is impossible to inspect what’s inside a WeakMap:
All WeakMap keys must be objects. You get an error if you use a
primitive value:
{
const obj = {};
obj.wm = 'attachedValue';
}
34.3 Examples
34.3.1 Caching computed results via
WeakMaps
If we use this function with an object obj, you can see that the result
is only computed for the first invocation, while a cached value is used
for the second invocation:
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
34.4 WeakMap API
The constructor and the four methods of WeakMap work the same as
their Map equivalents:
Quiz
Since ES6, JavaScript has the data structure Set, which can contain
arbitrary values and performs membership checks quickly.
35.1 Using Sets
35.1.1 Creating Sets
First, you can use the constructor without any parameters to create
an empty Set:
set.clear();
assert.equal(set.size, 0)
Sets are iterable and the for-of loop works as you’d expect:
Given that Sets are iterable, you can use spreading (...) to convert
them to Arrays:
assert.deepEqual(
[...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']));
35.3 What Set elements are
considered equal?
As with Map keys, Set elements are compared similarly to ===, with
the exception of NaN being equal to itself.
As with ===, two different objects are never considered equal (and
there is no way to change that at the moment):
> set.add({});
> set.size
1
> set.add({});
> set.size
2
35.4 Missing Set operations
Sets are missing several common operations. Such an operation can
usually be implemented by:
35.4.1 Union (a ∪ b)
Computing the union of two Sets a and b means creating a Set that
contains the elements of both a and b.
35.4.2 Intersection (a ∩ b)
35.4.3 Difference (a \ b)
assert.deepEqual([...difference], [1]);
Sets don’t have a method .map(). But we can borrow the one that
Arrays have:
Adds value to this Set. This method returns this, which means
that it can be chained.
Feeds each element of this Set to callback(). value and key both
contain the current element. This redundancy was introduced so
that this callback has the same type signature as the callback of
Map.prototype.forEach().
You can specify the this of callback via thisArg. If you omit it,
this is undefined.
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 and value are both the element.
Quiz
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 .add(), .delete(), .has(). Consult the section
on WeakMaps as black boxes for an explanation of why
WeakSets don’t allow iteration, looping, and clearing.
Given that we can’t iterate over their elements, there are not that
many use cases for WeakSets. They do enable us to mark objects.
36.1 Example: Marking objects as
safe to use with a method
Domenic Denicola shows how a class Foo can ensure that its methods
are only applied to instances that were created by it:
class Foo {
constructor() {
foos.add(this);
}
method() {
if (!foos.has(this)) {
throw new TypeError('Incompatible object!');
}
}
}
assert.throws(
() => {
const obj = {};
Foo.prototype.method.call(obj); // throws an exception
},
TypeError
);
36.2 WeakSet API
The constructor and the three methods of WeakSet work the same as
their Set equivalents:
Note that the pattern is “smaller” than the data: we are only
extracting what we need.
37.2 Constructing vs. extracting
In order to understand what destructuring is, consider that
JavaScript has two kinds of operations that are opposites:
assert.deepEqual(jane1, jane2);
const jane = {
first: 'Jane',
last: 'Doe',
};
Variable declarations:
Assignments:
let b;
[b] = ['z'];
assert.equal(b, 'z');
Parameter definitions:
In the next two sections, we’ll look deeper into the two kinds of
destructuring: object-destructuring and Array-destructuring.
37.4 Object-destructuring
Object-destructuring lets you batch-extract values of properties via
patterns that look like object literals:
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 value 'Evergreen Terrace' is assigned to the
pattern variable s.
Exercise: Object-destructuring
exercises/destructuring/object_destructuring_exrc.mjs
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});
let prop;
assert.throws(
() => eval("{prop} = { prop: 'hello' };"),
{
name: 'SyntaxError',
message: 'Unexpected token =',
});
Why eval()?
let prop;
({prop} = { prop: 'hello' });
assert.equal(prop, 'hello');
37.5 Array-destructuring
Array-destructuring lets you batch-extract values of Array elements
via patterns that look like Array literals:
The first element of the Array pattern in line A is a hole, which is why
the Array element at index 0 is ignored.
assert.equal(x, 'a');
assert.equal(y, 'b');
assert.deepEqual(remaining, ['c', 'd']);
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');
37.6.3 Object-destructuring: multiple
return values
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.
assert.throws(
() => { const {prop} = undefined; },
{
name: 'TypeError',
message: "Cannot destructure property `prop` of " +
"'undefined' or 'null'.",
}
);
assert.throws(
() => { const {prop} = null; },
{
name: 'TypeError',
message: "Cannot destructure property `prop` of " +
"'undefined' or 'null'.",
}
);
assert.throws(
() => { const [x] = {}; },
{
name: 'TypeError',
message: '{} is not iterable',
}
);
Quiz: basic
Here, we have two default values that are assigned to the variables x
and y because the corresponding elements don’t exist in the Array
that is destructured.
assert.equal(x, 1);
assert.equal(y, 2);
The default value for the first element of the Array pattern is 1; the
default value for the second element is 2.
37.10.2 Default values in object-
destructuring
Neither property key first nor property key last exist in the object
that is destructured. Therefore, the default values are used.
function f2(...args) {
const [«pattern1», «pattern2»] = args;
// ···
}
37.12 Nested destructuring
Until now, we have only used variables as assignment targets (data
sinks) inside destructuring patterns. But you can also use patterns as
assignment targets, which enables you to nest patterns to arbitrary
depths:
const arr = [
{ first: 'Jane', last: 'Bond' },
{ first: 'Lars', last: 'Croft' },
];
const [, {first}] = arr;
assert.equal(first, 'Lars');
Quiz: advanced
function* genFunc1() {
yield 'a';
yield 'b';
}
Like return, a yield exits the body of the function and returns a
value (via .next()).
Unlike return, if you repeat the invocation (of .next()),
execution resumes directly after the yield.
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;
}
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
iterators always deliver their values.
assert.deepEqual(
iter.next(), {value: 'b', done: false});
// genFunc2() is now paused directly after the second `yield`:
assert.equal(location, 2);
assert.deepEqual(
iter.next(), {value: undefined, done: true});
// We have reached the end of genFunc2():
assert.equal(location, 3);
/**
* 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++;
}
}
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
exercises/sync-generators/filter_iter_gen_test.mjs
38.2 Calling generators from
generators (advanced)
38.2.1 Calling generators via yield*
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 naive approach fails:
function* bar() {
yield 'a';
yield 'b';
}
function* foo() {
// Nothing happens if we call `bar()`:
bar();
}
assert.deepEqual(
[...foo()], []);
Why doesn’t this work? The function call bar() returns an iterable,
which we ignore.
function* bar() {
yield 'a';
yield 'b';
}
function* foo() {
yield* bar();
}
assert.deepEqual(
[...foo()], ['a', 'b']);
function* foo() {
for (const x of bar()) {
yield x;
}
}
function* gen() {
yield* [1, 2];
}
assert.deepEqual(
[...gen()], [1, 2]);
class BinaryTree {
constructor(value, left=null, right=null) {
this.value = value;
this.left = left;
this.right = right;
}
exercises/sync-generators/iter_nested_arrays_test.mjs
38.3 Background: external
iteration vs. internal iteration
In preparation for the next section, we need to learn about two
different styles of iterating over the values “inside” an object:
External iteration (pull): Your code asks the object for the values
via an iteration protocol. For example, the for-of loop is based
on JavaScript’s iteration protocol:
function logPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(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
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?
function* iterPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(dir, fileName);
yield filePath; // (A)
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
yield* iterPaths(filePath);
}
}
}
const paths = [...iterPaths('mydir')];
38.5 Advanced features of
generators
The chapter on generators in Exploring ES6 covers two features that
are beyond the scope of this book:
Normal functions are synchronous: the caller waits until the callee is
finished with its computation. divideSync() in line A is a
synchronous 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 mouse, add tasks to the queue.
function main() {
divideCallback(12, 3,
(err, result) => {
if (err) {
assert.fail(err);
} else {
assert.equal(result, 4);
}
});
}
divideCallback(x, y, callback)
unction h(z) {
const error = new Error();
console.log(error.stack);
unction g(y) {
h(y + 1);
unction f(x) {
g(x + 1);
(3);
/ done
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:
Error:
at h (demos/async-js/stack_trace.mjs:2:17)
at g (demos/async-js/stack_trace.mjs:6:3)
at f (demos/async-js/stack_trace.mjs:9:3)
at demos/async-js/stack_trace.mjs:11: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 location inside h().
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 like an implicit return. If we consider the
code fragment to be a task that is executed, then returning with an
empty call stack ends the task.
39.3 The event loop
By default, JavaScript runs in a single process – in both web
browsers and Node.js. The so-called event loop sequentially executes
tasks (pieces of code) inside that process. The event loop is depicted
in fig. 30.
Event loop ↺
Task queue
Figure 30: Task sources add code to run to the task queue, which is
emptied by the event loop.
Task sources add tasks to the queue. Some of those sources run
concurrently to the JavaScript process. For example, one task
source takes care of user interface events: if a user clicks
somewhere and a click listener was registered, then an
invocation of that listener is added to the task queue.
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 executes it. And so on.
while (true) {
const task = taskQueue.dequeue();
task(); // run task
}
39.4 How to avoid blocking the
JavaScript process
39.4.1 The user interface of the browser
can be blocked
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 browser/JavaScript process is blocked.
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;
}
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 therefore executed sometime after the current piece of code (task)
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 code logs 'end' before 'callback', even
though the parameter ms is 0.
39.5 Patterns for delivering
asynchronous results
In order to avoid blocking the main process while waiting for a long-
running operation to finish, results are often delivered
asynchronously in JavaScript. These are three popular patterns for
doing so:
Events
Callbacks
Promises
The first two patterns are explained in the next two subsections.
Promises are explained in the next chapter.
The parameters for the operation are provided via the request
object, not via parameters of the method. For example, the event
listeners (functions) are stored in the properties .onsuccess and
.onerror.
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, then activate it (line E). The configuration consists of:
function clickListener(event) {
event.preventDefault(); // (C)
console.log(event.shiftKey); // (D)
}
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 found in the second parameter.
The following exercises use tests for asynchronous code, which are
different from tests for synchronous code. Consult §11.3.2
“Asynchronous tests in AVA” for more information.
Recommended reading
addAsync(3, 4)
.then(result => { // success
assert.equal(result, 7);
})
.catch(error => { // failure
assert.fail(error);
});
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);
}
});
}
Pending Fulfilled
Rejected
Promise.resolve(123)
.then(x => {
assert.equal(x, 123);
});
Promise.resolve('abc')
.then(str => {
return str + str; // (A)
})
.then(str2 => {
assert.equal(str2, 'abcabc'); // (B)
});
Promise.resolve('abc')
.then(str => {
return Promise.resolve(123); // (A)
})
.then(num => {
assert.equal(num, 123);
});
// Flat
asyncFunc1()
.then(result1 => {
/*···*/
return asyncFunc2();
})
.then(result2 => {
/*···*/
});
// Nested
asyncFunc1()
.then(result1 => {
/*···*/
asyncFunc2()
.then(result2 => {
/*···*/
});
});
The only difference between .then() and .catch() is that the latter is
triggered by rejections, not fulfillments. However, both methods turn
the actions of their callbacks into Promises in the same manner. For
example, in the following code, the value returned by the .catch()
callback in line A becomes a 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 sources at the same time:
asyncFunc1()
.then(result1 => {
// ···
return asyncFunction2();
})
.then(result2 => {
// ···
})
.catch(error => {
// Failure: handle errors of asyncFunc1(), asyncFunc2()
// and any (sync) exceptions thrown in previous callbacks
});
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 object. First, a callback-based version. Second, a Promise-
based version.
The following code reads the contents of this file and converts it to a
JavaScript object. It is based on Node.js-style callbacks:
readFileAsync('person.json')
.then(text => { // (A)
// Success
const obj = JSON.parse(text);
assert.deepEqual(obj, {
first: 'Jane',
last: 'Doe',
});
})
.catch(err => { // (B)
// Failure: file I/O error or JSON syntax error
assert.fail(err);
});
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);
});
exercises/promises/promise_timeout_test.mjs
Exercises: util.promisify()
Using util.promisify():
exercises/promises/read_file_async_exrc.mjs
Implementing util.promisify() yourself:
exercises/promises/my_promisify_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');
});
exercises/promises/fetch_json_test.mjs
40.3 Error handling: don’t mix
rejections and exceptions
Rule for implementing functions and methods:
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 => {
// ···
});
}
// 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 => {
// ···
});
}
40.4 Promise-based functions start
synchronously, settle
asynchronously
Most Promise-based functions are executed as follows:
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 the code, while the result is delivered later (line A).
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']);
});
If and when all input Promises are fulfilled, the output Promise
is fulfilled with an Array of the fulfillment values.
As soon as at least one input Promise is rejected, the output
Promise is rejected with the rejection value of that input
Promise.
function concurrentAll() {
return Promise.all([asyncFunc1(), asyncFunc2()]);
}
function concurrentThen() {
const p1 = asyncFunc1();
const p2 = asyncFunc2();
return p1.then(r1 => p2.then(r2 => [r1, r2]));
}
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([
// Fork async computations
httpGet('http://example.com/file1.txt'),
httpGet('http://example.com/file2.txt'),
])
// Join async computations
.then(([text1, text2]) => {
assert.equal(text1, 'Content of file1.txt\n');
assert.equal(text2, 'Content of file2.txt\n');
});
function timesTwoSync(x) {
return 2 * x;
}
const arr = [1, 2, 3];
const result = arr.map(timesTwoSync);
assert.deepEqual(result, [2, 4, 6]);
function timesTwoAsync(x) {
return new Promise(resolve => resolve(x * 2));
}
const arr = [1, 2, 3];
const promiseArr = arr.map(timesTwoAsync);
Promise.all(promiseArr)
.then(result => {
assert.deepEqual(result, [2, 4, 6]);
});
function downloadTexts(urls) {
const promisedTexts = urls.map(httpGet);
return Promise.all(promisedTexts);
}
downloadTexts([
'http://example.com/file1.txt',
'http://example.com/file2.txt',
])
.then(texts => {
assert.deepEqual(
texts, [
'Content of file1.txt\n',
'Content of file2.txt\n',
]);
});
exercises/promises/list_files_async_test.mjs
40.6 Tips for chaining Promises
This section gives tips for chaining Promises.
Problem:
// Don’t do this
function foo() {
const promise = asyncFunc();
promise.then(result => {
// ···
});
return promise;
}
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);
}
});
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)
});
})
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().
40.7 Advanced topics
In addition to Promise.all(), there is also Promise.race(), which
is not used often and described in Exploring ES6.
Exploring ES6 has a section that shows a very simple
implementation of Promises. That may be helpful if you want a
deeper understanding of how Promises work.
41 Async functions
Roughly, async functions provide better syntax for code that uses
Promises. In order to use async functions, we should therefore
understand Promises. They are explained in the previous chapter.
41.1 Async functions: the basics
Consider the following async function:
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',
});
});
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!'));
});
asyncFunc()
.then(result => assert.equal(result, 'abc'));
The Promise p for the result is created when the async function
is started.
Then the body is executed. There are two ways in which
execution can leave the body:
Execution can leave permanently while settling p:
A return fulfills p.
A throw rejects p.
Execution can also leave temporarily when awaiting the
settlement of another Promise q via await. The async
function is paused and execution leaves it. It is resumed
once q is settled.
Promise p is returned after execution has left the body for the
first time (permanently or temporarily).
// Output:
// 'asyncFunc() starts'
// 'Task ends'
// 'Resolved: abc'
41.3 await: working with Promises
The await operator can only be used inside async functions and async
generators (which are explained in §42.2 “Asynchronous
generators”). Its operand is usually a Promise and leads to the
following steps being performed:
try {
await Promise.reject(new Error());
assert.fail(); // we never get here
} catch (e) {
assert.equal(e instanceof Error, true);
}
try {
await new Error();
assert.fail(); // we never get here
} catch (e) {
assert.equal(e instanceof Error, true);
}
exercises/async-functions/fetch_json2_test.mjs
The reason is that normal arrow functions don’t allow await inside
their bodies.
exercises/async-functions/map_async_test.mjs
41.4 (Advanced)
All remaining sections are advanced.
41.5 Immediately invoked async
arrow functions
If we need an await outside an async function (e.g., at the top level of
a module), then we can immediately invoke an async arrow function:
/**
* 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;
}
// Output:
// 'START first'
// 'END first'
// 'START second'
// 'END second'
// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'
await longRunningAsyncOperation();
console.log('Done!');
Here, we are using await to join a long-running asynchronous
operation. That ensures that the logging really happens after that
operation is done.
42 Asynchronous iteration
Required knowledge
Promises
Async functions
42.1 Basic asynchronous iteration
42.1.1 Protocol: async iteration
interface Iterable<T> {
[Symbol.iterator]() : Iterator<T>;
}
interface Iterator<T> {
next() : IteratorResult<T>;
}
interface IteratorResult<T> {
value: T;
done: boolean;
}
interface AsyncIterable<T> {
[Symbol.asyncIterator]() : AsyncIterator<T>;
}
interface AsyncIterator<T> {
next() : Promise<IteratorResult<T>>; // (A)
}
interface IteratorResult<T> {
value: T;
done: boolean;
}
Warning: We’ll soon see the solution for this exercise in this
chapter.
exercises/async-
iteration/async_iterable_to_array_test.mjs
42.2 Asynchronous generators
An asynchronous generator is two things at the same time:
(async () => {
const asyncIterable = yield123();
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
assert.deepEqual(
await asyncIterator.next(),
{ value: 1, done: false });
assert.deepEqual(
await asyncIterator.next(),
{ value: 2, done: false });
assert.deepEqual(
await asyncIterator.next(),
{ value: 3, done: false });
assert.deepEqual(
await asyncIterator.next(),
{ value: undefined, done: true });
})();
We wrapped the code in an immediately invoked async arrow
function.
Warning: We’ll soon see the solution for this exercise in this
chapter.
exercises/async-iteration/number_lines_test.mjs
Exercise: filterAsyncIter()
exercises/async-iteration/filter_async_iter_test.mjs
42.3 Async iteration over Node.js
streams
42.3.1 Node.js streams: async via
callbacks (push)
function main(inputFilePath) {
const readStream = fs.createReadStream(inputFilePath,
{ encoding: 'utf8', 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.
This time, the reader is in control and pulls data from the stream.
/**
* Parameter: async iterable of chunks (strings)
* Result: async iterable of lines (incl. newlines)
*/
async function* chunksToLines(chunksAsync) {
let previous = '';
for await (const chunk of chunksAsync) { // input
previous += chunk;
let eolIndex;
while ((eolIndex = previous.indexOf('\n')) >= 0) {
// line includes the EOL (Windows '\r\n' or Unix '\n')
const line = previous.slice(0, eolIndex+1);
yield line; // output
previous = previous.slice(eolIndex+1);
}
}
if (previous.length > 0) {
yield previous;
}
}
Availability of features
/abc/ui
\ ^ $ . * + ? ( ) [ ] { } |
> /\//.test('/')
true
Comments:
You can only use Unicode property escapes if the flag /u is set.
Without /u, \p is the same as p.
Forms (3) and (4) can be used as abbreviations if the property is
General_Category. For example, the following two escapes are
equivalent:
\p{Lowercase_Letter}
\p{General_Category=Lowercase_Letter}
Examples:
> /^\p{Script=Greek}+$/u.test('μετά')
true
Further reading:
43.2.5 Groups
43.2.6 Quantifiers
43.2.7 Assertions
43.2.7.1 Lookahead
Positive lookahead: (?=«pattern») matches if pattern matches
what comes next.
^aa|zz$ matches all strings that start with aa and/or end with zz.
Note that | has a lower precedence than ^ and $.
^(aa|zz)$ matches the two strings 'aa' and 'zz'.
^a(a|z)z$ matches the two strings 'aaz' and 'azz'.
43.3 Flags
Table 20: These are the regular expression flags supported by
JavaScript.
Literal Property
ES Description
flag name
g global ES3 Match multiple times
i ignoreCase ES3 Match case-insensitively
m multiline ES3 ^ and $ match per line
s dotall ES2018 Dot matches line
terminators
u unicode ES6 Unicode mode
(recommended)
y sticky ES6 No characters between
matches
RegExp.prototype.test()
RegExp.prototype.exec()
String.prototype.match()
> /a/.test('A')
false
> /a/i.test('A')
true
> 'a1\na2\na3'.match(/^a./gm)
[ 'a1', 'a2', 'a3' ]
> 'a1\na2\na3'.match(/^a./g)
[ 'a1' ]
> /./.test('\n')
false
> /./s.test('\n')
true
> /[^]/.test('\n')
true
> /pa-:/.test('pa-:')
true
Without /u, there are some pattern characters that still match
themselves if you escape them with backslashes:
> /\p\a\-\:/.test('pa-:')
true
With /u:
The following subsections explain the last item in more detail. They
use the following Unicode character to explain when the atomic units
are Unicode characters and when they are JavaScript characters:
With /u, the two code units of 🙂 are treated as a single character:
> /^[🙂]$/u.test('🙂')
true
> /^[\uD83D\uDE42]$/.test('\uD83D\uDE42')
false
> /^[\uD83D\uDE42]$/.test('\uDE42')
true
Note that ^ and $ demand that the input string have a single
character. That’s why the first result is false.
> '🙂'.match(/./gu).length
1
> '\uD83D\uDE80'.match(/./g).length
2
43.3.1.3 Consequence: quantifiers apply to Unicode
characters, not JavaScript characters
> /^🙂{3}$/u.test('🙂🙂🙂')
true
> /^\uD83D\uDE80{3}$/.test('\uD83D\uDE80\uDE80\uDE80')
true
43.4 Properties of regular
expression objects
Noteworthy:
> /a/i.ignoreCase
true
> /a/.ignoreCase
false
.dotall (/s)
.global (/g)
.ignoreCase (/i)
.multiline (/m)
.sticky (/y)
.unicode (/u)
43.4.2 Other properties
> /abc/ig.source
'abc'
> /abc/ig.flags
'gi'
> /a/.test('__a__')
true
You can change that by using assertions such as ^ or by using the flag
/y:
> /^a/.test('__a__')
false
> /^a/.test('a__')
true
> /bc/.test('ABCD')
false
> /bc/i.test('ABCD')
true
> /\.mjs$/.test('main.mjs')
true
With .test() you should normally avoid the /g flag. If you use it, you
generally don’t get the same result every time you call the method:
The results are due to /a/ having two matches in the string. After all
of those were found, .test() returns false.
The string method .search() returns the first index of str at which
there is a match for regExp:
> '_abc_'.search(/abc/)
1
> 'main.mjs'.search(/\.mjs$/)
4
Without the flag /g, .exec() returns the captures of the first match
for regExp in str:
assert.deepEqual(
/(a+)b/.exec('ab aab'),
{
0: 'ab',
1: 'a',
index: 0,
input: 'ab aab',
groups: undefined,
}
);
assert.deepEqual(
/(?<as>a+)b/.exec('ab aab'),
{
0: 'ab',
1: 'a',
index: 0,
input: 'ab aab',
groups: { as: 'a' },
}
);
In the result of .exec(), you can see that a named group is also a
positional group – its capture exists twice:
Once as a positional capture (property '1').
Once as a named capture (property groups.as).
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'
exercises/regexps/extract_quoted_test.mjs
43.5.5 str.match(regExp):
return all
matching substrings [ES3]
With /g, .match() returns all substrings of str that match regExp:
> 'xyz'.match(/(a+)b/g)
null
43.5.6 str.replace(searchValue,
replacementValue) [ES3]
If searchValue is:
Regular expression without /g: Replace first match of this
regular expression.
Regular expression with /g: Replace all matches of this
regular expression.
String: Replace first occurrence of this string (the string is
interpreted verbatim, not as a regular expression). Alas,
there is no way to replace every occurrence of a string. Later
in this chapter, we’ll see a tool function that converts a
string into a regular expression that matches this string
(e.g., '*' becomes /\*/).
If replacementValue is:
String: Replace matches with this string. The character $
has special meaning and lets you insert captures of groups
and more (read on for details).
Function: Compute strings that replace matches via this
function.
Text Result
$$ single $
$& complete match
Text Result
$` text before match
$' text after match
$n capture of positional group n (n > 0)
$<name> capture of named group name [ES2018]
Example: Inserting the text before, inside, and after the matched
substring.
assert.equal(
'3 cats and 4 dogs'.replace(/[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 included how one might name them:
exercises/regexps/change_quotes_test.mjs
RegExp.prototype.exec()
RegExp.prototype.test()
Then they can be called repeatedly and deliver all matches inside a
string. Property .lastIndex of the regular expression is used to track
the current position inside the string – for example:
const r = /a/g;
assert.equal(r.lastIndex, 0);
The next subsections explain the pitfalls of using /g. They are
followed by a subsection that explains how to work around those
pitfalls.
let count = 0;
// Infinite loop
while (/a/g.test('babaa')) {
count++;
}
If code expects a regular expression with /g and has a loop over the
results of .exec() or .test(), then a regular expression without /g
can cause an infinite loop:
function countMatches(regExp) {
let count = 0;
// Infinite loop
while (regExp.exec('babaa')) {
count++;
}
return count;
}
countMatches(/a/); // Missing: flag /g
Why? Because .exec() always returns the first result, a match object,
and never null.
function isMatching(regExp) {
return regExp.test('Xa');
}
const myRegExp = /^X/g;
assert.equal(isMatching(myRegExp), true);
assert.equal(isMatching(myRegExp), false);
function countMatches(regExp) {
let count = 0;
while (regExp.exec('babaa')) {
count++;
}
return count;
}
countMatches(regExp, str)
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.
function countMatches(regExp, str) {
const cloneFlags = regExp.flags + (regExp.global ? '' : 'g');
const clone = new RegExp(regExp, cloneFlags);
let count = 0;
while (clone.test(str)) {
count++;
}
return count;
}
function escapeForRegExp(str) {
return str.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); // (A)
}
assert.equal(escapeForRegExp('[yes?]'), String.raw`\[yes\?\]`);
assert.equal(escapeForRegExp('_g_'), String.raw`_g_`);
The regular expression method .replace() only lets you replace plain
text once. With escapeForRegExp(), we can work around that
limitation and replace plain text multiple times:
> /(?:)/.test('')
true
> /(?:)/.test('abc')
true
> /.^/.test('')
false
> /.^/.test('abc')
false
44 Dates (Date)
This chapter describes JavaScript’s API for working with dates – the
class Date.
44.1 Best practice: avoid the built-
in Date
The JavaScript Date API is cumbersome to use. Hence, it’s best to
rely on a library for anything related to dates. Popular libraries
include:
Moment.js
Day.js
Luxon
js-joda
date-fns
Consult the blog post “Why you shouldn’t use Moment.js…” for the
pros and cons of these libraries.
UTC, Z, and GMT are ways of specifying time that are similar, but
subtly different:
Sources:
'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
THH:mm+HH:mm (etc.)
THH:mm-HH:mm (etc.)
const timeValue = 0;
assert.equal(
new Date(timeValue).toISOString(),
'1970-01-01T00:00:00.000Z');
assert.equal(
new Date('1972-05-03') < new Date('2001-12-23'), true);
// Internally:
assert.equal(73699200000 < 1009065600000, true);
Returns the time value for the specified UTC date time.
Date.prototype.setTime(timeValue) (UTC)
Example:
Note that the input hours (21) are different from the output hours
(20). The former refer to the local time zone, the latter to UTC.
If there is not 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
FullYear
Month: month (0–11). Pitfall: 0 is January, etc.
Date: day of the month (1–31)
Day (getter only): day of the week (0–6, 0 is Sunday)
Time
Hours:hour (0–23)
Minutes: minutes (0–59)
Seconds: seconds (0–59)
Milliseconds: milliseconds (0–999)
Date.prototype.getTimezoneOffset()
Returns the time difference between local time zone and UTC in
minutes. For example, for Europe/Paris, it returns -120 (CEST,
Central European Summer Time) or -60 (CET, Central European
Time):
> d.toTimeString()
'01:00:00 GMT+0100 (Central European Standard Time)'
> d.toDateString()
'Thu Jan 01 1970'
> d.toString()
'Thu Jan 01 1970 01:00:00 GMT+0100 (Central European Standar
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'
The following three methods are not really part of ECMAScript, but
rather of the ECMAScript internationalization API. That API has
much functionality for formatting dates (including support for time
zones), but not for parsing them.
Date.prototype.toLocaleTimeString()
Date.prototype.toLocaleDateString()
Date.prototype.toLocaleString()
exercises/dates/create_date_string_test.mjs
45 Creating and parsing
JSON (JSON)
{
"first": "Jane",
"last": "Porter",
"married": true,
"born": 1890,
"friends": [ "Tarzan", "Cheeta" ]
}
Compound:
Object literals:
Property keys are double-quoted strings.
Property values are JSON values.
No trailing commas are allowed.
Array literals:
Elements are JSON values.
No holes or trailing commas are allowed.
Atomic:
null (but not undefined)
Booleans
Numbers (excluding NaN, +Infinity, -Infinity)
Strings (must be double-quoted)
assert.equal(
JSON.stringify({foo: ['a', 'b']}),
'{"foo":["a","b"]}' );
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'
> JSON.stringify(undefined)
undefined
> JSON.stringify(Symbol())
undefined
Objects:
> JSON.parse('{"foo":["a","b"]}')
{ foo: [ 'a', 'b' ] }
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) );
Converting a point to JSON: JSON.stringify() internally calls
the previously mentioned method .toJSON().
assert.equal(
JSON.stringify(new Point(3, 5)),
'{"x":3,"y":5}' );
exercises/json/to_from_json_test.mjs
45.4 Customizing stringification
and parsing (advanced)
Stringification and parsing can be customized as follows:
JSON.parse(text, reviver?)
const obj = {
a: 1,
b: {
c: 2,
d: 3,
}
};
assert.equal(
JSON.stringify(obj, ['b', 'c']),
'{"b":{"c":2}}');
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 },
]);
const obj = {
name: 'abc',
regex: /abc/ui,
};
assert.equal(
JSON.stringify(obj),
'{"name":"abc","regex":{}}');
You now know most of the JavaScript language. This chapter gives
an overview of web development and describes next steps. It answers
questions such as:
How can you avoid feeling overwhelmed when faced with this
constantly changing vastness of knowledge?
Focus on the web technologies that you work with most often
and learn them well. If you do frontend development, that may
be JavaScript, CSS, SVG, or something else.
For JavaScript: Know the language, but also try out one tool in
each of the following categories (which are covered in more
detail later).
Compilers: compile future JavaScript or supersets of
JavaScript to normal JavaScript.
Bundlers: combine all modules used by a web app into a
single file (a script or a module). That makes loading faster
and enables dead code elimination.
Static checkers. For example:
Linters: check for anti-patterns, style violations, and
more.
Type checkers: type JavaScript statically and report
errors.
Test libraries and tools
Version control (usually git)
Given that it is just a virtual machine, there are not that many
practically relevant things to learn about WebAssembly. But it is
worth keeping an eye on its evolving role in web development. It is
also becoming popular as a stand-alone virtual machine; e.g.,
supported by the WebAssembly System Interface.
46.3 Example: tool-based
JavaScript workflow
<script src="code.js">
<script src="library.js">
loads loads
Figure 32: A classic, very simple web app: An HTML file refers to a
JavaScript file code.js, which imbues the former with interactivity.
code.js uses the library library.js, which must also be loaded by the
HTML file.
Fig. 32 depicts a classic web app – when web development was less
sophisticated (for better and for worse):
Entry
imports imports
compiled to compiled to
ad
de
dt
added to
o
to
ed
add
Output
loads
<script src="bundle.js">
Figure 33: This is the workflow when developing a web app with the
bundler webpack. Our web app consists of multiple modules. We tell
webpack, in which one execution starts (the so-called entry point). It
then analyzes the imports of the entry point, the imports of the
imports, etc., to determine what code is needed to run the app. All of
that code is put into a single script file.
The basic structure is still the same: the HTML file loads a JavaScript
script file via a <script> element. However:
The code is now modular without the HTML file having to know
the modules.
bundle.js only includes the code that is needed to run the app
(vs. all of library.js).
We used a package manager to install the libraries that our code
depends on.
The libraries aren’t accessed via global variables but via ES
module specifiers.
let numberOfOccurrences = 5;
if (Math.random()) {
// Math.random() is not zero
numberOfOccurrences++
}
let a=5;Math.random()&&a++;
All of these tools and build steps are usually coordinated via so-called
task runners (think “make” in Unix). There are:
46.4.3 Testing
There are alternatives to npm, but they are all based in one way or
another on npm’s software registry:
46.4.5 Libraries
Various helpers: lodash (which was originally based on the
Underscore.js library) is one of the most popular general helper
libraries for JavaScript.
Data structures: The following libraries are two examples among
many.
Immutable.js provides immutable data structures for
JavaScript.
Immer is an interesting lightweight alternative to
Immutable.js. It also doesn’t mutate the data it operates on,
but it works with normal objects and Arrays.
Date libraries: JavaScript’s built-in support for dates is limited
and full of pitfalls. The chapter on dates lists libraries that you
can use instead.
Internationalization: In this area, ECMAScript’s standard library
is complemented by the ECMAScript Internationalization API
(ECMA-402). It is accessed via the global variable Intl and
available in most modern browsers.
Implementing and accessing services: The following are two
popular options that are supported by a variety of libraries and
tools.
REST (Representative State Transfer) is one popular option
for services and based on HTTP(S).
GraphQL is more sophisticated (for example, it can
combine multiple data sources) and supports a query
language.
46.5 Tools not related to
JavaScript
Given that JavaScript is just one of several kinds of artifacts involved
in web development, more tools exist. These are but a few examples:
CSS:
Minifiers: reduce the size of CSS by removing comments,
etc.
Preprocessors: let you write compact CSS (sometimes
augmented with control flow constructs, etc.) that is
expanded into deployable, more verbose CSS.
Frameworks: provide help with layout, decent-looking user
interface components, etc.
Images: Automatically optimizing the size of bitmap images, etc.
47 Index
Symbol
!x
++x
x++
+x
, (comma operator)
--x
x--
-x
x && y
x + y
x - y
x / y
x << y
x === y
x >>> y
x >> y
x & y
x ** y
x * y
x ^ y
x ¦ y
x ¦¦ y
x ٪ y
=
c ? t : e
__proto__
~x
big endian
binary integer literal
binding (variable)
bitwise And
bitwise Not
bitwise Or
bitwise Xor
boolean
Boolean()
bound variable
break
bundler
bundling
C
call stack
callback (asynchronous pattern)
callback function
camel case
catch
class
class
class declaration
class definition
class expression
class, mixin
classes, private data for
closure
code point
code unit
coercion
comma operator
CommonJS module
comparing by identity
comparing by value
computed property key
concatenating strings
conditional operator
console
console.error()
console.log()
const
constant
constructor function (role of an ordinary function)
continue
Converting to [type]
Coordinated Universal Time (UTC)
copy object deeply
copy object shallowly
currying
dash case
DataView
date
date time format
decimal floating point literal
decimal integer literal
decrementation operator (prefix)
decrementation operator (suffix)
deep copy of an object
default export
default value (destructuring)
default value (parameter)
delete
deleting a property
dense Array
descriptor of a property
destructive operation
destructuring
destructuring an Array
destructuring an object
dictionary (role of an object)
direct method call
dispatched method call
divided by operator
division
do-while
dynamic this
dynamic vs. static
early activation
Ecma
ECMA-262
ECMAScript
ECMAScript module
Eich, Brendan
endianness (Typed Arrays)
enumerability
enumerable (property attribute)
environment (variables)
equality operator
ES module
escaping HTML
eval()
evaluating an expression
event (asynchronous pattern)
event loop
exception
exercises, getting started with
exponentiation
export
export default
export, default
export, named
expression
extends
external iteration
extracting a method
false
falsiness
falsy
finally
flags (regular expression)
Float32Array
Float64Array
floating point literal
for
for-await-of
for-in
for-of
free variable
freezing an object
fulfilled (Promise state)
function declaration
function expression, anonymous
function expression, named
function, arrow
function, ordinary
function, roles of an ordinary
function, specialized
function*
garbage collection
generator, asynchronous
generator, synchronous
getter (object literal)
global
global object
global scope
global variable
globalThis
GMT (Greenwich Mean Time)
grapheme cluster
Greenwich Mean Time (GMT)
heap
hexadecimal integer literal
hoisting
hole in an Array
identifier
identity of an object
if
IIFE (immediately invoked function expression)
immediately invoked function expression (IIFE)
import
import()
import, named
import, namespace
in
incrementation operator (prefix)
incrementation operator (suffix)
index of an Array
Infinity
inheritance, multiple
inheritance, single
instanceof
instanceof
Int16Array
Int32Array
Int8Array
integer
integer, safe
internal iteration
iterable (asynchronous)
iterable (synchronous)
iteration, asynchronous
iteration, external
iteration, internal
iteration, synchronous
iterator (asynchronous)
iterator (synchronous)
kebab case
keyword
label
left shift operator
let
lexical this
listing properties
little endian
logical And
logical Not
logical Or
M
Map
Map
Map vs. object
Math(namespace object)
method
method (object literal)
method (role of an ordinary function)
method call, direct
method call, dispatched
method, extracting a
minification
minifier
minus operator (binary)
minus operator (unary)
mixin class
module specifier
module, AMD
module, CommonJS
multidimensional Array
multiple inheritance
multiple return values
multiplication
named export
named function expression
named import
named parameter
namespace import
NaN
node_modules
npm
npm package
null
number
Number()
object
object literal
object vs. Map
object vs. primitive value
Object()
object, copy deeply
object, copy shallowly
object, freezing an
object, identity of an
object, roles of an
object-destructuring
Object.is()
octal integer literal
ordinary function
ordinary function, roles of an
override a property
P
package, npm
package.json
parameter
parameter default value
parameter vs. argument
partial application
passing by identity
passing by value
pattern (regular expression)
pending (Promise state)
plus operator (binary)
plus operator (unary)
polyfill
polyfill, speculative
ponyfill
primitive value
primitive value vs. object
private data for classes
progressive web app
prollyfill
Promise
Promise, states of a
properties, listing
property (object)
property attribute
property descriptor
property key
property key, computed
property key, quoted
property name
property symbol
property value shorthand
property, deleting a
prototype
prototype chain
publicly known symbol
safe integer
scope of a variable
script
self
sequence (role of an Array)
Set
Set
setter (object literal)
settled (Promise state)
shadowing
shallow copy of an object
shim
signed right shift operator
single inheritance
sloppy mode
snake case
sparse Array
specialized function
specifier, module
speculative polyfill
spreading (...) into a function call
spreading into an Array literal
spreading into an object literal
statement
states of a Promise
static
static vs. dynamic
strict mode
string
String()
subclass
subtraction
switch
symbol
symbol, publicly known
synchronous generator
synchronous iterable
synchronous iteration
synchronous iterator
syntax
tagged template
task queue
task runner
TC39
TC39 process
TDZ (temporal dead zone)
Technical Committee 39
template literal
temporal dead zone
ternary operator
this
this, dynamic
this, lexical
this, pitfalls of
this, values of
throw
time value
times operator
to the power of operator
transpilation
transpiler
tree-shaking
true
truthiness
truthy
try
tuple (role of an Array)
type
type hierarchy
type signature
Typed Array
typeof
TypeScript
U
Uint16Array
Uint32Array
Uint8Array
Uint8ClampedArray
undefined
underscore case
Unicode
Unicode Transformation Format (UTF)
unit test
unsigned right shift operator
UTC (Coordinated Universal Time)
UTF (Unicode Transformation Format)
UTF-16
UTF-32
UTF-8
variable, bound
variable, free
variable, scope of a
void operator
Wasm (WebAssembly)
WeakMap
WeakMap
WeakSet
WeakSet
Web Worker
WebAssembly
while
window
wrapper types (for primitive types)