How JavaScript Works Master The Basics of JavaScript and Modern Web App Development (Jonathon Simpson)
How JavaScript Works Master The Basics of JavaScript and Modern Web App Development (Jonathon Simpson)
Works
Master the Basics of JavaScript and
Modern Web App Development
—
Jonathon Simpson
How JavaScript Works
Master the Basics of JavaScript
and Modern Web App
Development
Jonathon Simpson
How JavaScript Works: Master the Basics of JavaScript and Modern Web
App Development
Jonathon Simpson
Belfast, Antrim, UK
Introduction����������������������������������������������������������������������������������������xv
iii
Table of Contents
Assignment Operators����������������������������������������������������������������������������������������26
Variable Concatenation����������������������������������������������������������������������������������27
JavaScript Comments�����������������������������������������������������������������������������������������29
Logical Statements���������������������������������������������������������������������������������������������30
If…else Statements��������������������������������������������������������������������������������������30
Switch Statements����������������������������������������������������������������������������������������33
Block Scoping with Logical Statements��������������������������������������������������������36
Conditional Operator in Variables������������������������������������������������������������������38
Logical Statement Comparison Operators�����������������������������������������������������38
Logical Statement Logical Operators������������������������������������������������������������41
Summary������������������������������������������������������������������������������������������������������������42
iv
Table of Contents
v
Table of Contents
Chapter 7: Types�������������������������������������������������������������������������������131
Primitive Types��������������������������������������������������������������������������������������������������131
Primitive Wrappers��������������������������������������������������������������������������������������������133
Using Wrappers to Create Types�����������������������������������������������������������������������136
The Number Type and NaN�������������������������������������������������������������������������������137
Number Type Mathematics��������������������������������������������������������������������������139
Mathematical Methods��������������������������������������������������������������������������������141
The Date Type����������������������������������������������������������������������������������������������������143
The Symbol Type�����������������������������������������������������������������������������������������������147
Truthy and Falsy Types��������������������������������������������������������������������������������������149
vi
Table of Contents
vii
Table of Contents
viii
Table of Contents
ix
Table of Contents
Index�������������������������������������������������������������������������������������������������315
x
About the Author
Jonathon Simpson studied at UCL and currently works in product
development at Revolut, a global neobank and financial technology
company that offers banking services. He has over 15 years of web
development experience working on a wide range of products and
services. Jonathon also owns and operates a popular software engineering
blog focusing on JavaScript and web development.
xi
About the Technical Reviewer
Russ Ferguson is a web application developer living in Brooklyn, New York.
He has worked on projects for organizations such as Ann Taylor, MTV,
DC Comics, and LG. Currently, he is the Vice President at Bank of America
managing a team of Angular developers, building internal applications.
xiii
Introduction
JavaScript is one of the most used programming languages in the
world. When JavaScript was first created, it was a useful tool for adding
interactivity to web pages. Since then, it has evolved to power back-end
servers, massive front-end web applications, and even iPhone and Android
applications via tools like Electron and Tauri.
While JavaScript has matured as a language its complexity seems to
have increased. What started as simple scripts inside HTML tags now
seems to involve compile steps via TypeScript and frameworks like
React, Vue.js, or Svelte. For those getting started in JavaScript, it can be
overwhelming, even though these tools are just an additional level of
abstraction which ultimately compiles down into vanilla JavaScript.
In this book, we’ll learn how JavaScript works from the bottom up,
which will prepare you for everything web development and JavaScript-led
app development can throw at you. We’ll also explain some of the quirks
you’ll find in JavaScript and how many of them have come to be due to
JavaScript’s long and varied history.
After that, we’ll cover JavaScript’s unique approach to inheritance
before moving into more complicated topics like memory management,
classes, APIs, and web workers. We’ll explore how the weakly typed system
JavaScript employs has both benefits and downsides.
As a fundamental part of the web stack, and with more people using
it every day, there has never been a better time to learn JavaScript. This
book will guide you through everything you need to know, so that you can
master modern web app development.
xv
CHAPTER 1
Introduction
to JavaScript
JavaScript is a programming language that first appeared in 1995 as the
scripting language for the Netscape browser. Since then, it has evolved into
one of the most used programming languages in the world. While its initial
goal was to add interactivity to websites, it has since come to do just about
everything, including creating desktop apps and back-end APIs.
JavaScript is everywhere, and over the years, many frameworks have
been built on top of it, such as jQuery, React, Vue.js, and Svelte. All of
this can make learning JavaScript intimidating, as there are often many
different ways to achieve the same thing.
In this book, we’ll be covering how JavaScript works at a fundamental
level. That will then make it is easier to understand how frameworks
like React and Vue.js work. We’ll discuss why things work the way they
do in JavaScript and the various quirks that come with years of ongoing
development on the language.
JavaScript today broadly falls into two major categories:
JavaScript Fundamentals
JavaScript is based on a language standard called ECMAScript. How
JavaScript should exactly work is documented in a specific standard called
ECMA-262. Since ECMAScript does not provide any tools to compile
JavaScript, every implementation of JavaScript creates its own version
of JavaScript. That includes your browser and back-end compilers like
Node.js.
For the most part, these implementations follow ECMA-262, but
since each implementation is done by different teams, there can be some
minor discrepancies or different feature sets depending on the browser or
implementation.
In this chapter, we will be covering how you can set yourself up to start
using JavaScript, including how to set up JavaScript projects when using
Node.js. In future chapters, we will explore how to write JavaScript code.
2
Chapter 1 Introduction to JavaScript
You’ll see that no types are defined here. For example, we did not have
to mention that "Some String" was a String. JavaScript determines types
based on context – so it will take x to be a String simply because we put
its value in quotation marks. Similarly, it will dynamically interpret y as
being of type Number since it lacks quotation marks and z as being of type
Boolean since it has no quotation marks and uses the keyword false.
This makes JavaScript quite easy to pick up, but quite hard to master.
The lack of strong typing can mean that you unknowingly create bugs in
your software since JavaScript will not always throw errors if unexpected
types show up, and even worse, JavaScript may dynamically interpret types
incorrectly in some cases.
3
Chapter 1 Introduction to JavaScript
For more complex applications with lots of test cases, developers often
reach for TypeScript instead of JavaScript for this reason. TypeScript
is JavaScript, but extended. It’s strongly typed, meaning types must be
mentioned in your code.
4
Chapter 1 Introduction to JavaScript
Writing JavaScript
JavaScript on the front end is found inside HTML on web pages. As such,
familiarity with HTML is quite important when we work with JavaScript. To
create your first file containing JavaScript, you can start by making a .html
file. We usually call the home page of a website index.html when building
websites, so for this example, I created a new HTML file called index.html.
.html files can be opened by any web browser, such as Google
Chrome. You can edit your HTML file by opening it up in a text
or code editor (Notepad included), and puting in this standard
“boilerplate” HTML:
<!DOCTYPE html>
<html>
<head>
<title>My First JavaScript</title>
</head>
<body>
<p>Hello World</p>
5
Chapter 1 Introduction to JavaScript
<script type=”text/javascript”>
// This is JavaScript!
</script>
</body>
</html>
Note You can use any text editor (like Notepad) to create HTML
and even JavaScript. While that works fine, it’s better to use a
professional code editor instead. One of the most popular code
editors used in the software development community is VS Code.
You can download it via https://code.visualstudio.com/.
This will color code your JavaScript and give you a lot of other useful
features.
<script type="text/javascript">
// This is JavaScript!
</script>
Since JavaScript applications can get quite long, you may see this
<script> tag substituted out for a file instead. This can be useful since it
lets us separate our HTML and JavaScript into different files.
For example, if we had a separate JavaScript file called myScript.js
stored in the same folder as index.html file, we could load that into our
HTML document by using the src attribute on the script tag:
<script src="myScript.js"></script>
6
Chapter 1 Introduction to JavaScript
You may also see JavaScript embedded into HTML via the attributes
of HTML tags. For example, JavaScript can be put inside a button to cause
something to happen when a user clicks that button:
7
Chapter 1 Introduction to JavaScript
Figure 1-1. After you install VS Code, open the application. By going
to File ➤ Open Folder… or clicking “Open Folder” in VS Code and
finding your “My JavaScript” folder
8
Chapter 1 Introduction to JavaScript
9
Chapter 1 Introduction to JavaScript
Running the node command lets you compile and execute JavaScript
files. Let’s try it out – create a file called index.js in a code editor or
Notepad, and add the following JavaScript code before saving:
console.log("Hello World!")
Then you can execute this file by using the node command in terminal:
node index.js
This will produce an output which looks something like what is shown
in Figure 1-3.
10
Chapter 1 Introduction to JavaScript
Note If you saved your index.js file in another directory, you will
need to provide the full directory link. To navigate directories, use
the cd command. For example, if your index.js file was in “/Users/
JohnDoe/Documents/”, you would run cd /Users/JohnDoe/
Documents/ and only after that, run node index.js.
cd ~/Desktop/node-project
After that’s done, you can use the npm init command, which is
installed along with Node.js, to initiate your project. You can see how that
looks in Figure 1-4.
11
Chapter 1 Introduction to JavaScript
Figure 1-4. When using the npm init command, you will be asked to
enter some information about your new project as shown earlier
All you have to do now is type in answers to each question and press
enter. For example, the first question asks what you want to call your
project – so type in the name of your project, and press enter.
Your folder will now contain a package.json file summarizing the
information you provided. Since you’ve initialized your Node.js project,
you’ll now be able to run other commands like npm install now, which
lets you install third party dependencies.
JavaScript Support
Traditional software is usually written by a developer and downloaded
onto a user’s computer or device. This is the case with things like video
games, apps on your phone, or big applications like Adobe Photoshop.
When writing code in JavaScript, things are very different. The software
the user installs is the browser, not your website! The browser then loads
12
Chapter 1 Introduction to JavaScript
your web page within it. Since everyone has their own browser preference,
and not everyone keeps their browsers up to date, JavaScript that works in
one browser can oftentimes not work in another. For example, Firefox may
support a new JavaScript feature, but Chrome may not. The worst thing
about this is you can’t really use a new JavaScript feature on the front end
until a majority of browsers have implemented it.
If you are coming from other languages, then worrying about browser
support will be a foreign concept to you. In JavaScript, it is a real thing.
In recent times, since most browsers are “evergreen” (meaning they
auto-update), this has become less of a problem than it used to be, but
sometimes different browsers just disagree on what should and shouldn’t
be implemented. Promising new features may end up implemented in just
Chrome, just Safari, or just Firefox.
Throughout this book, we’ll only be looking at JavaScript with broad
browser support, meaning you don’t need to worry about if you can or
can’t use it. However, when you start exploring JavaScript in your own
time, and especially when looking at more advanced functionality, it’s
important to check if browsers support it. You can find good browser
support tables on websites like https://caniuse.com/ or https://
developer.mozilla.org/.
An example of a browser support table can be found in Figure 1-5, for
the GPU feature.
13
Chapter 1 Introduction to JavaScript
Figure 1-5. Not all browsers support every new JavaScript feature. In
the preceding example, only Chrome and Edge have support. Other
browsers only have partial support or none at all. That means if you
tried to implement this on a website, only some users could use it!
Summary
In this chapter, we’ve looked at how to set up your workspace to begin
writing code with JavaScript. We’ve discussed what JavaScript is typically
used for and some of the pitfalls or differences between it and other
languages. Now that we’ve covered the basics let’s look at how to write
JavaScript code.
14
CHAPTER 2
Getting Started
As we go through this chapter, it will be good to have a work space where
you can write and test your JavaScript. For these purposes, I’ve created a
folder called “javascript-project” in my documents folder. Within that, I
have created two files – index.html and index.js.
Since our focus will be writing JavaScript, your HTML file can be
relatively simple:
<!DOCTYPE html>
<html>
<head>
<title>My First JavaScript</title>
</head>
<body>
<p>Hello World</p>
<script src="index.js"></script>
</body>
</html>
Any JavaScript code you want to try out can be put in index.js. For
now, I’ve only put a simple console.log method:
console.log("Hello World!")
16
Chapter 2 Code Structure and Logical Statements
Semicolons
For readability, JavaScript is sometimes written with a semicolon at the
end of each line. For example:
console.log("Hello World!");
console.log("Goodbye World!");
However, this is not necessary, and it’s also just as common to see the
same code written without semicolons:
console.log("Hello World!")
console.log("Goodbye World!")
console.log(5 +
6)
For the code in this book, we will be omitting the semicolon unless it is
really needed.
17
Chapter 2 Code Structure and Logical Statements
Spacing
One of the most vigorously fought over code conventions is whether to use
tabs or spaces for indenting. While there is essentially no right or wrong
answer to this, it is important to be consistent. If you use tabs, then always
use tabs for indents and likewise for spaces.
Unlike Python, indents play no functional role in JavaScript, but they
do serve to make your code more readable when others look at it. While
it is certainly fine to use tabs, spaces are going to cause you less of a
headache. That’s because different ecosystems and operating systems can
be configured to handle tabs differently, whereas spaces are consistently
sized across all systems.
In this book, we will indent with four spaces. Here is an example of
how that will look:
let myVariable = 5
if(myVariable === 5) {
console.log("The variable is 5!")
}
Note If you use the tab key instead of spaces in VS Code to indent
your code, you can configure VS Code to automatically convert these
to spaces if you want it to. The configuration option is found by
opening any file and selecting the “Spaces/Tab Size” option in the
bottom right-hand corner.
18
Chapter 2 Code Structure and Logical Statements
When naming variables and functions, all of these are fine to use, but
again, it is important to be consistent. If you decide to use camel case, then
make sure you use it everywhere. For the purposes of this book, we will be
using camel case.
JavaScript Variables
As we begin writing JavaScript, the first thing you’re going to need to learn
about are variables. Variables are a way to assign a fixed name to a data
value. There are three ways to create a variable in JavaScript, using three
different keywords:
• var
• let
• const
19
Chapter 2 Code Structure and Logical Statements
let myVariable = 5
console.log(myVariable)
Note It may seem like we are putting data into the variable, but a
better way to think about it is we are making data and then pointing
the keyword “myVariable” at the data we just made.
It’s not possible to assign a variable with let twice. If you think of your
variable as pointing to some data, it’s easy to see why – the same variable
can’t point to two different pieces of data. For example, the following code
will produce the error, which is shown in Figure 2-2.
let myVariable = 5
let myVariable = 10
console.log(myVariable)
20
Chapter 2 Code Structure and Logical Statements
let myVariable = 5
myVariable = 10
console.log(myVariable)
It may seem like the data has changed (or “mutated”), but actually we’ve
just made new data somewhere and pointed our variable to that instead. The
data “5” still exists somewhere. It just has no variable pointing to it anymore.
When a piece of data is no longer referenced in our code, JavaScript may
remove it from memory using something called “garbage collection.” This
allows JavaScript to free up memory when data is no longer used.
let myVariable = 5
{
let myVariable = 10
console.log(myVariable)
}
console.log(myVariable)
21
Chapter 2 Code Structure and Logical Statements
Figure 2-3. While using let, our console produces two different lines,
5 and 10. This is because let is assigned to its current scope – so setting
myVariable to 10 in a separate scope does not affect the original
variable. If we used var instead, both lines would say 10 since scope
is ignored
var myVariable = 5
22
Chapter 2 Code Structure and Logical Statements
The reason why we use let rather than var is because var has a few
quirks that let does not. For example, you can define a variable twice with
var, and no errors will be thrown:
var myVariable = 5
var myVariable = 10
console.log(myVariable)
Variables defined with var are also not block-scoped, meaning your
code can produce some odd results when redefining variables in block
scopes with var.
You may be wondering, “Why does JavaScript have two ways of
defining variables when let is a more controlled version of var?” The
answer to that is pretty simple, and it’s because var used to be the only
way to define variables in JavaScript, and a lot of legacy code uses it. Later
on, JavaScript created a better way to define variables using let, but var
couldn’t be removed since it would break many older code bases.
let myVariable = 5
myVariable = 10
console.log(myVariable)
23
Chapter 2 Code Structure and Logical Statements
While this works for let, it will not work for const. Variables defined
with const are constants and cannot be reassigned:
const myConst = 5
console.log(myConst)
If you try to reassign the value of a const variable, you’ll get an error instead.
const myConst = 5
myConst = 10
console.log(myConst)
Using const to define variables is better when you’re able to use it. To
understand why, you can think about the example we discussed earlier where
we used let to reassign a variable from “5” to “10”. We talked about how the
value “5” still exists in memory unless it gets garbage collected. Since const
variables cannot be changed, garbage collection never has to run. That means
less cleanup is required, resulting in more efficient memory utilisation.
24
Chapter 2 Code Structure and Logical Statements
Arrays can contain a lot of data, and we can push new data to an array
using a special method called push:
By using push we can mutate our array, meaning the underlying data
changes and the data continues to be referenced and stored in the same
place. In other words, we did not create new data and point our variable
somewhere else, but instead mutated the original data.
This is confusing to beginners since the array was pointed to by a const
variable, so it would therefore be assumed that since the const variable is a
constant, the data inside must always remain constant. This is not the case
in JavaScript. So, in summary, while reassignment must remain constant in
a const variable, data mutation is fine.
let myVariable
console.log(myVariable)
This is sometimes done when a variable does not have a value when
you declare it but may be assigned a value later on in the code. When many
variables need to be declared without values, they can be separated by
commas. In the following example, we define three variables with the let
keyword:
25
Chapter 2 Code Structure and Logical Statements
Assignment Operators
Now that we’ve covered the basics of setting variables, let’s look at
assignment operators. These allow us to modify an existing variable, by
changing its value. For example, consider this variable:
let x = 5
let x = 5
x *= 5
console.log(x) // Console logs 25 (5 multiplied by 5 = 25)
There are many other assignment operators. They are shown in the
following example:
let x = 5
x *= 5
console.log(x) // Console logs 25 (5 multiplied by 5 = 25)
x += 5
console.log(x) // Console logs 30 (25 plus 5 = 30)
26
Chapter 2 Code Structure and Logical Statements
x /= 5
console.log(x) // Console logs 6 (30 divided by 5 = 6)
x -= 1
console.log(x) // Console logs 5 (6 minus 1 = 5)
x %= 4
console.log(x)
/*
Console logs 1 (if you divide 5 by 4, the remainder is 1.
% is the remainder operator
*/
Variable Concatenation
When we have variables that consist of at least one string, using the +
operator causes the strings to become concatenated. To understand this,
take a look at the following example, where we concatenate two strings
into a new variable:
// "hello world"
let combine = myVariable + " " + myOtherVariable
27
Chapter 2 Code Structure and Logical Statements
Just be careful, since if you try to use a + with numbers, it will add them
up instead!
let myVariable = 5
let myOtherVariable = 5
Different types of data have different built-in methods, which we’ll look
at in much more detail in future chapters.
Template Literals
Another final way to concatenate more elegantly is through a type
of functionality called template literals. Template literals are still strings, but
they use the backtick "`" to transform any content into a template literal.
Template literals have the added benefit of allowing line breaks – something
28
Chapter 2 Code Structure and Logical Statements
that numbers and quotation marks do not. They also allow for substitution.
Here is an example of a particularly messy template literal with line breaks
throughout:
Template literals like the preceding one will be taken with line breaks
and white space included – which means you’ll avoid loss of this content
when using them. They also allow for substitution. Adding a variable in ${}
will substitute it into a template literal:
JavaScript Comments
You may have already noticed that in the preceding code, I used double
slashes to leave some comments on what the code does. As our code
gets more complicated, it’s useful to leave messages to yourself or other
developers about what is going on. For this purpose, comments are used.
A comment in JavaScript can take one of two forms. The first looks
like this:
// I am a comment!
And the second looks like this, where the comment is enclosed in
/* and */:
/* I am a comment! */
29
Chapter 2 Code Structure and Logical Statements
Both work the same, but the second allows for multiline comments.
Comments have no bearing on the functionality of your code and instead
can provide useful information on what’s going on. For example, we could
comment our previous code like so:
Logical Statements
Now that we’ve covered variables, you’ll probably be wondering how we
can use them. Logical statements are one of the ways we can start to put
our variables to good use.
Logical statements are used when we want to check if something is
true or false and then run a particular action based on that outcome. If a
logical statement is found to be true, everything within its “block scope”
is executed. Likewise, if a logical statement is false, then the block scope
will never run at all. In other words, logical statements allow us to build
conditionality into our programs.
If…else Statements
if...else are perhaps one of the most commonly used logical statements
around. All they do is check if a statement is true. If it is, they run some
code, else, they run some other code. In the following example, we check
if myVariable is set to 5. If it is, we show “The variable is 5!” in the console.
If it’s not, then we show alternative text.
30
Chapter 2 Code Structure and Logical Statements
You can view this code running in Figure 2-4, where I run it directly
from the console in my web browser. You could also try this out by
updating your index.js file from earlier:
if(myVariable === 5) {
console.log("The variable is 5!")
}
else {
console.log("The variable is not 5.")
}
31
Chapter 2 Code Structure and Logical Statements
if(myVariable === 5) {
console.log("The variable is 5!")
}
else if(myVariable === 6) {
myVariable = 7
console.log("The variable is 6, but I set it to 7!")
}
else {
console.log("The variable is not 5.")
}
As you can see in the else if condition, any code can be written in
the block scope created by a logical statement. In else if, we reassigned
myVariable to have a value of 7.
If the output of an if statement is one line of code, you can omit the
curly brackets and write it all on one line of code as shown in the following
example. This also applies to else if and else:
32
Chapter 2 Code Structure and Logical Statements
Switch Statements
Another common way to create conditionality in your JavaScript is through
switch statements. They take a single variable and check if it equals
certain values using the case keyword to define the value it might equal
and a break to declare the end of that clause. For example:
// Let's set x to 5
let x = 5
switch(x) {
case 5:
console.log("hello")
break
case 6:
console.log("goodbye")
break
}
// Let's set x to 5
let x = 5
switch(x) {
case 4:
console.log("4!")
case 5:
console.log("hello")
33
Chapter 2 Code Structure and Logical Statements
case 6:
console.log("goodbye")
}
Figure 2-5. Since we didn’t use the keyword “break” in the preceding
code and x is equal to 5, both “hello” and “goodbye” are console
logged. The “break” keyword is important for declaring the end of a
clause in a switch statement.
let x = "Apples"
switch(x) {
case "Apples":
case "Strawberries":
34
Chapter 2 Code Structure and Logical Statements
// Let's set x to 5
let x = 5
switch(x) {
case 4: {
console.log("hello")
break
}
default: {
console.log("goodbye")
break
}
}
35
Chapter 2 Code Structure and Logical Statements
“5” have the same value but different types. As such, only “goodbye” is
shown in the console:
// Let's set x to 5
let x = 5
switch(x) {
case "5": {
console.log("hello")
break
}
case 5: {
console.log("goodbye")
break
}
}
36
Chapter 2 Code Structure and Logical Statements
// Let's set x to 5
let x = 5
switch(x) {
case 5:
let x = 6
console.log("hello")
break
case 6:
let x = 5
console.log("goodbye")
break
}
// Let's set x to 5
let x = 5
switch(x) {
case 5: {
let x = 6
console.log("hello")
break
}
case 6: {
let x = 5
console.log("goodbye")
break
}
}
37
Chapter 2 Code Structure and Logical Statements
let x = 5
if(myVariable === 5)
switch(variable)
That means both the value and the type of data have to match for
the statement to be true. For example, '5' is not the same type as 5 even
though the values are the same since the types are different:
38
Chapter 2 Code Structure and Logical Statements
let myVariable = 5
if(myVariable === 5) // True!
if(myVariable === "5") // False!
Besides strict equality, there are lots of other ways we can compare
data in logical statements. In the following example, we use the more than
operator to check if myVariable is more than specific values. The outcome
of this code can be seen in Figure 2-6.
let myVariable = 5
if(myVariable > 4) console.log("Hello World") // True!
if(myVariable > 6) console.log("Hello World 2!") // False!
We can check for “regular” equality too, which ignores type, using the
double equals (==) operator:
let myVariable = 5
if(myVariable == '5') // True!
39
Chapter 2 Code Structure and Logical Statements
=== Strict equality. Both the type and value of the 5 === 5 // True
data must be the same. 5 === '5' // False
== Regular equality. Only the value must be the 5 == 5 // True
same. 5 == '5' // True
> More than. The value on the left must be more 6 > 5 // True
than the right. 5 > 5 // False
4 > 5 // False
>= More than or equal to. The value on the left 6 >= 5 // True
must be more than or equal to the one on the 5 >= 5 // True
right. 4 >= 5 // False
< Less than. The value on the left must be less 6 < 5 // False
than the one on the right. 5 < 5 // False
4 < 5 // True
<= Less than or equal to. The value on the left 6 <= 5 // False
must be less than or equal to the one on the 5 <= 5 // True
right. 4 <= 5 // True
!== Strict not equal to. The two values should not 5 !== 5 // False
match value and type. 5 !== '5' // True
5 !== 4 // True
!= Regular not equal to. The two values should 5 !== 5 // False
not match value. 5 !== '5' // False
5 !== 4 // True
40
Chapter 2 Code Structure and Logical Statements
let x = 6
if(x > 5 && x <= 10) {
console.log('Hello World!')
}
let x = 6
if(x > 5 || x > 3) {
console.log('Hello World!')
}
41
Chapter 2 Code Structure and Logical Statements
We can also use the NOT operator to let us invert a result. For example,
in the following we check if x is NOT more than 5 (by inverting x > 5):
let x = 6
if(!(x > 5)) {
console.log('Hello World!')
}
Summary
In this chapter, we’ve covered some of the most important fundamental
concepts of JavaScript. This has included some of the best practices for
writing your code. We’ve also looked at how you can use variables to point
to data and the difference between mutating data versus reassigning it.
We’ve learned how to update those variables via assignment operators
and how they can be used in a variety of logical statements. We have also
discussed comments and how to add them to your code. All of these
concepts are really important and required for the following chapters. As
such, having a good grasp on this is critical to writing more complicated
JavaScript.
42
CHAPTER 3
Introduction to
Objects, Arrays
Now that we’ve now looked at the fundamental concepts of JavaScript,
let’s move onto one of the most important data types in JavaScript, that
being objects. Objects are widely used in JavaScript as a store of data. They
differ from other types of data in that they are the only type of data which is
mutable.
In this chapter, we’ll be covering the basics of how objects work. We’ll
also be covering arrays, which are a special type of object with specific
features. As we get more comfortable with objects and arrays, we’ll look at
some more complicated concepts such as prototype-based programming
and object inheritability.
Arrays
To start this chapter, let’s look at arrays. Arrays are a special type of
object which are used to store one-dimensional data. They have no explicitly
defined keys. Arrays are usually defined inside [] square brackets which is
different from objects, which we’ll cover in more detail later, since they use
{ } curly brackets. In the following example, we create an array that contains
a mixture of different types of data, including an object:
44
Chapter 3 Introduction to Objects, Arrays
One example of these is the length property, which is used to get the
size of an array. Methods and properties on arrays can be accessed by
using a dot, followed by that property or method’s name. For example,
to get the size of a given array, we use the .length method directly on
that array.
Let’s try putting the length of myArray into a new variable called
arrayLength. When we console log this, it’ll show 4:
45
Chapter 3 Introduction to Objects, Arrays
The .at() method works a lot like retrieving an array element with
square brackets. The main difference is that if you use negative numbers,
it starts counting from the opposite end – so .at(-1) also gets the last
element of an array:
46
Chapter 3 Introduction to Objects, Arrays
47
Chapter 3 Introduction to Objects, Arrays
Splice
Adding and removing elements from the start and end of an array is useful,
but more often than not, we’ll want to change something in the middle of
an array. To do this, we can use another method called splice(). Unlike
the methods we have seen so far, splice() takes a couple of arguments,
although only one is required. Since only one argument is required, the
syntax for splice can look like any of the following variations:
someArray.splice(start)
someArray.splice(start, end)
someArray.splice(start, end, item1)
someArray.splice(start, end, item1, item2, item3 ... itemN)
48
Chapter 3 Introduction to Objects, Arrays
If we only use the first argument in splice, then every element after a
certain point in the array will be deleted, as is shown in the following example:
Finally, if we use the arguments after start and end, we can add new
items in specific parts of our array. Let’s add “strawberry” and “box” in
between “banana” and “cake”, using the third and fourth arguments. Any
argument given after the end argument will be added to the array at the
start position:
Objects
Now that we’ve covered arrays, let’s move onto objects. Objects are similar
to arrays, but they differ in that they have defined keys. Objects are arguably
the most important thing you can understand in JavaScript, and they lead
into more advanced concepts on how we write and structure our code. In
this section, we’ll look into how objects work and how they are used more
broadly in prototypical inheritance, which is the main programming style in
49
Chapter 3 Introduction to Objects, Arrays
JavaScript. JavaScript objects look a lot like what are called “Dictionaries” in
other languages and consist of defined key–value pairs. They can be defined
within curly brackets {}, as is shown in the following example:
let myObject = {
"key": "value",
"someKey": 5,
"anotherKey" : true
}
This object has three keys and three values assigned to each of those
keys. Keys have to be unique, but values do not, and objects can be as big
as you’d like them to be.
The curly brackets {}, are known as the “object literal” notation – so as
you’d expect, it is the equivalent of writing “new Object” (just like for Arrays).
Since objects have keys, however, if we want to use the constructor to define
an object, we need to write all the keys and values out separately and attach
them to the object, making the object literal notation infinitely easier to use.
In the following example, we create the same object as we did in our previous
example, but we use the object constructor instead of the object literal:
50
Chapter 3 Introduction to Objects, Arrays
let myObject = {
"key": "value",
"someKey": 5,
"anotherKey" : true
}
console.log(myObject["key"]) // shows 'value'
Figure 3-1. Using the square bracket notation, we can access the
value of any key on an object in JavaScript
51
Chapter 3 Introduction to Objects, Arrays
let myObject = {
"key": "value",
"someKey": 5,
"anotherKey" : true
}
console.log(myObject.key) // shows 'value'
When we use square brackets, we must use quotation marks if the key
is a string ("key", not key), but the same is not true for the dot notation.
As an example of this, consider the following code. With square
brackets, the keyName variable is used if we omit the quotation marks.
However, when using the dot, JavaScript will look for a key on myObject
called keyName, which of course, returns undefined. Therefore, both square
brackets and dot notation have different utilities when accessing objects:
let myObject = {
"key": "value",
"someKey": 5,
"anotherKey" : true
}
let keyName = "key"
console.log(myObject[keyName]) // shows "value"
console.log(myObject.keyName) // shows undefined
52
Chapter 3 Introduction to Objects, Arrays
Figure 3-2. Using the square bracket notation, we can access the
value of any key on an object in JavaScript
Destructuring Objects
We’ve now covered the many ways you can access data on an object.
Another useful way to do this is by destructuring the object. Destructuring
objects works by allowing us to split the object into a set of variables, each
of which can be used independently. To illustrate this, let’s look at an
example. First, let’s create a simple object:
const myObj = {
z: 5,
y: 4,
x: 3
}
53
Chapter 3 Introduction to Objects, Arrays
const myObj = {
z: 5,
y: 4,
x: 3
}
const { x, y } = myObj
console.log(y) // 4
This is a useful way to take an object and only access the bits
you need via variables. It’s particularly common when unwrapping
external packages in Node.js, but it’s also widely used in front-end client
JavaScript too. Destructuring can even give default values to undefined
ones. If a value is found to be undefined when destructuring, the default
will be used instead:
const myObj = {
z: undefined,
y: 4,
x: 3
}
const { z = 5 } = myObj
console.log(z) // 5
The variable names you use when destructuring must match the
property names, unless you are destructuring an array. In that case, you
can call your variables anything you like:
You can unwrap a set of objects together using the three dots operator,
as shown in the following example:
54
Chapter 3 Introduction to Objects, Arrays
const myObj = {
z: undefined,
y: 4,
x: 3
}
const { x, ...rest } = myObj
// Only shows z and y: { z: undefined, y: 4 }
console.log(rest)
Object Mutability
As we’ve discussed previously, objects are mutable even if contained
within a const variable. We can update any key on an object to something
else by using an equals sign to set it to something completely different:
let myObject = {
"key": "value",
"someKey": 5,
"anotherKey" : true
}
let myObject = {
"key": "value",
"someKey": 5,
"anotherKey" : true
}
55
Chapter 3 Introduction to Objects, Arrays
Objects may also contain other objects within them, and these can be
accessed via multiple square brackets or using the dot notation if you prefer:
let myObject = {
"key": {
"key" : 5,
"newKey" : "value"
},
"someKey": 5,
"anotherKey" : true
}
Non-mutable objects
The only time an object’s mutability changes is if you change it yourself. All
objects have three hidden properties that configure their mutability along
with their value:
By default, all of these properties are true. Some useful utility methods
exist to change these properties. For example, you can change an object to
read-only by using Object.freeze, after which no property changes will
56
Chapter 3 Introduction to Objects, Arrays
affect the object. In practice, all this does is sets the object to { writable:
false, configurable: false }. Another similar property, Object.seal,
will instead set an object to { configurable: false }. This means existing
properties can be changed but not delete, and new ones cannot be added.
In Figure 3-3, you can see how Object.freeze prevents an object from
being modified. No errors are thrown, but the change is ignored.
57
Chapter 3 Introduction to Objects, Arrays
Objects can also be merged in the same way – but if duplicate keys are
found, the second object will overwrite the first:
Using the spread syntax on an array inside of an object literal will turn
it into an object, too – where the keys are the indices of the array. This
provides a simple way to convert an array to an object:
Prototype-Based Programming
When we started this chapter, we mentioned that all arrays have the
method .at(). The reason for this is that arrays are created by initiating a
new instance of an array via new Array(). When a new instance of an array
is made, it inherits all properties and methods of the Array object, which
includes .at().
Inheritance in JavaScript happens via something we call prototypes, a
special part of all objects that allows for inheritance. While other languages
use classes and object-oriented programming, the most common
paradigm in JavaScript is the use of prototypes. This type of programming
is called prototype-based programming.
All objects in JavaScript have a prototype, including arrays. You can see
that if you try to console log any object. In Figure 3-5, a console log of an
object I just created shows the [[Prototype]] property.
59
Chapter 3 Introduction to Objects, Arrays
But how do all arrays “inherit” the .at() method? Well first, it’s
important to remember that we said square brackets act as “syntactic
sugar” for the array constructor:
60
Chapter 3 Introduction to Objects, Arrays
Although so far we’ve been talking about arrays, the same is true for
all objects. Since all objects are new instances of a standard “object” type,
you can find all standard object methods by using console.log(Object.
prototype).
Prototypical Inheritance
We are now getting relatively familiar with objects and arrays and
hopefully gaining an understanding of how arrays and objects both
get their methods and properties. We also know how to access object
properties. For example, we know that given the following code, we could
access the property key by typing myObject.key:
let myObject = {
"key": "value",
"someKey": {
"someOtherKey" : 5
61
Chapter 3 Introduction to Objects, Arrays
},
"anotherKey" : true
}
We also saw earlier that we could use the Array.at() method to get an
array item at a specific index:
Well, the answer is of course no, and the preceding code will actually
produce an error. To understand why, we have to learn about how
JavaScript checks for properties on an object, as well as how JavaScript
copies prototypes to new instances. When we write myArray.at(-1),
JavaScript does the following:
62
Chapter 3 Introduction to Objects, Arrays
Figure 3-7. While a new array will inherit all prototype properties
and methods from its parent, they do not exist within a prototype
property. Instead, they sit in a special area called [[Prototype]]
63
Chapter 3 Introduction to Objects, Arrays
shouldn’t use it. If you do use it, it’s very slow, and since it’s not a standard
part of the JavaScript specification, you might run into some unexpected
problems. A console log for Object.prototype where you can see the
__proto__ property can be found in Figure 3-8.
In summary:
64
Chapter 3 Introduction to Objects, Arrays
This seems to make a lot of sense. The variable myVar has changed, so
of course, the original data should be different. However, things begin to
get tricky when you put the splice into another variable. When we do that,
you’ll find that the two variables have different values on the console log:
65
Chapter 3 Introduction to Objects, Arrays
While the values stored in myArr and newArr are now different, they still
reference the same underlying data. The data in the reference is different
since it has been changed by the slice method. Since we stored the slice
output on a variable, the underlying data never changed. This can make it
seem like both variables are working independently, but they are not!
If you try experimenting with this, you might get even more confused
because JavaScript only updates the underlying “value” (i.e., the DATA in
Figure 3-4) in certain circumstances. For example, updating the value of
only newArr, seems to only affect newArr:
66
Chapter 3 Introduction to Objects, Arrays
newArr[2] = "lightning"
console.log(myArr) // [ "lightning", "search", "key", "bolt" ]
console.log(newArr) // [ "key", empty, "lightning" ]
So if they are both working from the same data under the hood, why does
this not update the underlying value of our data? That’s because of something
that also seems strange, which is that if you use square brackets specifically, it
only assigns data to the shallow copy! If you instead use the dot notation, like
in situations where an object is in your array, both the shallow and original
will change since changing a property changes the underlying value:
// Update arrayOneSlice
newArr[0].items = [ "lightning" ]
let myArray = [ 1, 2, 3, 4 ];
let deepCopy = structuredClone(myArray);
67
Chapter 3 Introduction to Objects, Arrays
Deep copies give you certainty. In the previous example, if you apply
any methods to deepCopy, it will only affect deepCopy. The function,
structuredClone, can be used on any object or array to ensure it is a deep
copy rather than a shallow one.
Summary
In this chapter, we’ve covered the fundamentals of objects and arrays.
We’ve shown you how to construct your own objects and arrays, and
then how to mutate them using a variety of methods and operators. In
addition to this, we’ve looked at how prototypical inheritance works
and how objects form the backbone of JavaScript’s main programming
paradigm, prototype-based programming. We’ve also looked at the variety
of ways you can manipulate or access objects and how you can limit
access to objects via hidden properties like configurable. Finally, we look
at some of the quirks related to working with objects, such as deep and
shallow copies.
In future chapters, we’ll build on the themes we have learned here by
introducing new concepts.
68
CHAPTER 4
Loops
There are three forms of loops in JavaScript. The first we will look at is the
while clause. The while clause accepts one logical statement, and while
that logical statement is true, the code inside curly brackets will continue
to be executed. In the following example, the result is that console.log
will run ten times:
let x = 1
while(x < 10) {
console.log(x)
++x
}
Figure 4-1. A while loop will keep executing code until the logical
statement is no longer true. In this example, if you do not add or
multiply something by x, the logical statement will always remain
true, leading to an infinite loop and an error
The result of our preceding loop is shown in Figure 4-2. Since (x < 10)
remains true until x has had 10 added to it, we get 10 new lines on the
console log.
70
Chapter 4 Loops and Iterables
let x = 1
do {
console.log("hello world")
} while(x < 1)
71
Chapter 4 Loops and Iterables
The final way we can perform a loop is with a for statement. A for
statement combines the variable definition, logical statement, and
modification all in one statement. In the following example, we recreate
the while statement from Figure 4-1, but using for instead:
The for loop is tidier than a while loop since all of its conditions are
confined to one line. Functionally, though, it does the same thing that
while does. The modification defined in the for loop, that being ++x, will
only run after the body of the loop has been run. Table 4-1 summarizes
how the for loop works.
72
Chapter 4 Loops and Iterables
Since we break before the console log, this loop only shows 0, 2, 4,
6, 8. Another similar concept is continue, which instead of breaking the
entire loop, only breaks the current iteration.
Loop Labels
As our code gets more complicated, we can end up with loops inside
of loops. That can lead to some interesting problems when using breaks,
since break and continue do not know which loop you want to break or
continue on.
73
Chapter 4 Loops and Iterables
74
Chapter 4 Loops and Iterables
Note While I am using the labels xLoop and yLoop in the previous
example, you can label your loops in any way you see fit – these are
just made-up names!
Iteration
When using loops, we are typically using variables and logical statements
to iterate through statements until they stop returning true. This stops us
from repeating ourselves and simplifies our code.
In the previous chapter, we spoke about how objects and arrays act as a
store of data in JavaScript. Eventually, we’ll want to use the data contained
by them in our code. We can access each individually, but what if we want
75
Chapter 4 Loops and Iterables
To solve this problem, some data types in JavaScript are iterable. For
example, arrays and strings are both iterable, but objects are not. Other
types of data like maps and sets are also iterable, but we’ll cover those in
much more depth in future chapters.
for(let item of x) {
console.log(item)
}
76
Chapter 4 Loops and Iterables
Figure 4-4. The for...of loop will allow us to access every item of
an array or iterable, as shown in the preceding image
for...in, on the other hand, will refer to the index, and not the item
itself. In the following example, the output is 0, 1, 2, 3, whereas in
for...of, it was lightning, apple, squid, speaker as the output:
for(let item in x) {
console.log(item)
}
for...of and for...in differ in the way they handle undefined values.
For example, consider the following example where an array consists of
only one array item at location 5. When using for...of, it will return 4
undefined values, followed by “some value”:
let x = []
x[5] = "some value"
for(let item of x) {
console.log(item)
// Will show:
77
Chapter 4 Loops and Iterables
With for...in, we only get indices and not array values, so it therefore
omits any undefined values:
for(let item in x) {
console.log(item)
// Will show: 5
}
The forEach method provides the index, item, and entire array to you
at each iteration in your array. While this method is useful, it is slower than
a for loop – so using a for loop when you can is your best bet.
String Iteration
Since strings are also iterable, for...of and for...in also work on them. In the
following example, using for...of on the string “hello” produces an output
of all characters in that string. The following code console logs individually
each letter, so h,e,l,l,o. You can see this output in Figure 4-5.
78
Chapter 4 Loops and Iterables
Figure 4-5. Using for...of on a string will also iterate through each
character in that string. If you use for...in, you will get a number on
each line for each letter
Iteration Protocol
In the previous chapter, we discussed how different data types, like arrays,
inherit from a prototype. This is called prototypical inheritance.
When we define an array using new Array() or the square bracket
notation, we are making a new instance of Array, which inherits many
methods and properties from Array.prototype. Any data type, like arrays
or strings, which have iterability, will inherit a property called Symbol.
iterator from their prototype chain. This is known as the iteration
protocol. All types which have the iteration protocol are iterable. The
iteration protocol is therefore the reason why we can use for...in and
for...of on arrays and strings. You can see iteration protocol property in
Figure 4-6.
79
Chapter 4 Loops and Iterables
Figure 4-6. You can see the iterator protocol by console logging an
iterable’s prototype, like console.log(Array.prototype)
Another cool feature, which comes with the iteration protocol, is that
they allow the iterable to be stepped through. You can do this by accessing
the iteration protocol key, Symbol.iterator, directly. Since Symbol.
iterator is just another key inherited from Array.prototype, we can
access it just as we would have run myArray.at(-1):
80
Chapter 4 Loops and Iterables
If you keep running next(), you’ll keep getting the next item, as is
shown in the following example:
81
Chapter 4 Loops and Iterables
We haven’t seen these object methods before, but they are all available
on the global Object object. Consider the following example, where we
have an array called myObject. We can use both Object.keys() and
Object.values() directly on this object to extract an array from each:
let myObject = {
firstName: "John",
lastName: "Doe",
age: 140
}
82
Chapter 4 Loops and Iterables
Once we have extracted an array, we can run this through a for loop
since arrays are iterable. This is shown in the following example and also
in Figure 4-8.
let myObject = {
firstName: "John",
lastName: "Doe",
age: 140
}
83
Chapter 4 Loops and Iterables
let myObject = {
firstName: "John",
lastName: "Doe",
age: 140
}
84
Chapter 4 Loops and Iterables
Summary
In this chapter, we’ve covered all of the different kinds of loops in
JavaScript. We’ve also discussed how breaks, continues, and labels work in
the context of for loops. Looping is not just confined to logical statements,
so we’ve also gone into a lot of detail about how to iterate over arrays,
strings, and any other data type implementing the iteration protocol.
Objects, the most important data type in JavaScript, are not directly
iterable. However, we can convert them into iterable arrays through
methods like Object.entries().
Throughout the book, we’ll need to access arrays, objects, and other
iterables. Understanding these concepts makes writing code more
efficient, and we’ll use the code we’ve learned here in future chapters.
85
CHAPTER 5
References, Values,
and Memory
Management
We have already alluded to the fact that variables point to certain
references in memory, and we’ve also briefly covered how deep and
shallow copies of objects exist. In this chapter, we’ll be going into more
depth about how memory allocation actually work. All of the concepts we
will discuss in this chapter fall under a broad topic known as “memory
management.” Memory management, in simple terms, is how JavaScript
allocates data we create to memory.
Introduction
To understand memory management, we need to understand “heaps”
and “stacks.” These are both memory concepts and are both stored in the
“random access memory” or RAM.
Computers have a fixed amount of RAM. Since JavaScript stores data in
RAM, the amount of data your JavaScript uses impacts the amount of RAM
used on your computer or server. As such, it can be possible to run out of
RAM if you build a sufficiently complicated JavaScript application.
So if the heap and stack are both stored in RAM, and they’re both used
for storing JavaScript data, what’s the difference?
Stacks
The stack is a scratch space for the current JavaScript thread. Every time
you point to a primitive type (primitive, here, meaning anything which
is not an object) in JavaScript, it is added to the top of the stack. In the
following code, we define variables. Non-object type data is immediately
added to the top of the stack. A representation of this can be seen in
Figure 5-1.
88
Chapter 5 References, Values, and Memory Management
const SOME_CONSTANT = 5
let myNumber = 10
let myVariable = "Some Text"
Figure 5-1. Each new line of code is added to the stack. This includes
functions
let myNumber = 5
let newNumber = myNumber
89
Chapter 5 References, Values, and Memory Management
Note In this section we will briefly go into how APIs work to show
you how the event loop works. While we won’t cover what APIs are and
how to use them in detail here, we will go into this in future chapters.
Jump ahead if you are interested in learning more about APIs.
90
Chapter 5 References, Values, and Memory Management
“API” sounds complicated, but it’s just a URL that we can hit, which
will eventually send us a response from inside our code. For example, we
may hit the URL https://some-website.org/api/articles to retrieve
website articles. We would then receive a response from the API with all of
the articles. In a future chapter, we’ll deep dive into how APIs work.
When we run an API, the server will do some computation on our
behalf, but our JavaScript code will continue to run. Since the API and
JavaScript code are both processing at the same time, this gives us a way to
create multiple threads.
Browsers have some built-in APIs that can be called straight from your
code. These are known as Web APIs, and they usually offer an interface
between the code in your browser and the operating system itself. An
example of a Web API is the global function, setTimeout, which lets us
execute code after a certain number of seconds:
let myNumber = 5
setTimeout(function() {
console.log("Hello World")
}, 1000)
91
Chapter 5 References, Values, and Memory Management
Note Web API functions that take a long time to run, like
setTimeout, are added to the event loop as “macrotasks.” Some faster
Web API functions are added as “microtasks.” Microtasks take priority
when being added back to the main stack. Whether a Web API generates
a macro or microtask is dependent on how long it takes to run!
92
Chapter 5 References, Values, and Memory Management
The Heap
When we looked at objects, we covered how we can copy objects by
making “deep” and “shallow” copies of them. The reason we have deep
and shallow copies in JavaScript is because of how objects are stored in
the heap.
While non-object types are stored in the stack only, objects are thrown
into the heap, which is a more dynamic form of memory with no limit. This
means that large objects will never exceed the stack limit.
Consider the following object. First, we define a new object, and then
we set another variable to point to it:
While in previous examples using the stack, this type of code would
lead to two new entries, this code does something slightly different. Here,
the object is stored in the heap, and the stack only refers to the heap
reference. The preceding code would produce something a little bit like
what is shown in Figure 5-4.
Figure 5-4. Objects are stored in the heap, and the stack refers to the
reference of that object. The reference codes used here are illustrative
93
Chapter 5 References, Values, and Memory Management
Since the variables now refer to the same object, updating one can
have an effect on the other, which can cause confusion when updating
arrays that you believe are deep copies, but are actually shallow copies.
Note on object types Since functions and arrays are also of type
“object,” they too are stored in the heap!
let myNumber = 5
let newNumber = 5
let newObject = { name: "John Schmidt" }
let cloneObject = { name: "John Schmidt" }
let additionalObject = newObject
We’ve just defined a bunch of variables, and in the stack, it would look
a little like what’s shown in Figure 5-5.
94
Chapter 5 References, Values, and Memory Management
So, newNumber and myNumber both have the same value and will give us
a “true” value if we try to test their equality with the triple equals sign:
let myNumber = 5
let newNumber = 5
95
Chapter 5 References, Values, and Memory Management
96
Chapter 5 References, Values, and Memory Management
Summary
In this chapter, we’ve covered a lot of basic concepts on how memory
management works. We’ve looked at how stacks and heaps work in
JavaScript. We’ve also touched on Web APIs and how they simulate
multithreaded behavior in JavaScript. We’ve briefly covered APIs and Web
APIs, although we will look at these in much more detail in future chapters.
Topics like how JavaScript handles object equality can seem confusing
and counterintuitive until we understand how objects are stored and
compared differently than other data types. As such, a good understanding
of memory management can help explain some of the quirks we
experience when developing applications in JavaScript.
97
CHAPTER 6
Functions
and Classes
Functions and classes are what we use to wrap certain pieces of functionality
into reusable blocks. By using a function or class, we can repeat specific
tasks many times without having to rewrite the code. When JavaScript
was originally released, it only had functions and used prototypes for
inheritance. We covered this type of inheritance when we looked at objects.
Classes came later to JavaScript, but they are largely just syntactic sugar
for prototype-based inheritance. As such, many developers in JavaScript
choose not to use classes and instead depend on prototypical inheritance.
As with all things in software development, it doesn’t really matter if you
decide to use classes, or depend on prototypical inheritance with functions.
The important thing is that you are consistent throughout your projects.
Introduction to Functions
A typical function consists of three parts:
1. An input, known as arguments (although
sometimes, no input is given)
2. Some code to either manipulate those inputs or
perform some kind of action.
3. An output, usually related to the input. It doesn’t
have to be, but it’s better if it is.
© Jonathon Simpson 2023 99
J. Simpson, How JavaScript Works, https://doi.org/10.1007/978-1-4842-9738-4_6
Chapter 6 Functions and Classes
function myFunction() {
return "Hello World"
}
console.log(myFunction())
100
Chapter 6 Functions and Classes
101
Chapter 6 Functions and Classes
Figure 6-2. Functions are added to the top of the stack as they are
declared – just like variables
This is really useful in the real world, where we often have data stored
in arrays and objects, which we then want to pass to functions.
102
Chapter 6 Functions and Classes
console.log(words("Hello", "World"))
We can also put functions like this inside objects, allowing us to group
functions together or add methods to a prototype:
let wordFunctions = {
words: function(word1, word2) {
return word1 + " " + word2
}
}
Anonymous Functions
Anonymous functions (also sometimes referred to as Immediately Invoked
Function Expression or IIFEs) are functions that actually are unnamed
and are called immediately. To make it run immediately, we wrap it in
round brackets and call it with double round brackets as before:
(function(word1, word2) {
return word1 + " " + word2
})("Hello", "World")
103
Chapter 6 Functions and Classes
Arguments are put in the second set of brackets so that they can be
passed to the function. The use of anonymous functions is falling, but they
are sometimes still used to create a separate scope to work within.
Arrow notation functions are different in that they don’t store a unique
context. To understand what that means, we first have to understand
what the this keyword means in JavaScript and therefore a little bit about
strict mode.
104
Chapter 6 Functions and Classes
tells you the width of the user’s browser window. It also contains information
on mouse position, and it’s where all Web APIs exist.
As such, the two console logs in the following example show the
same thing:
Given what we’ve said so far, when we call the this keyword inside a
function, you might expect the this keyword to refer to the context of the
function, but you’ll find that it still shows the global this object:
console.log(this) // Window { }
let words = function (word1, word2) {
console.log(this) // Window { }
return word1 + " " + word2
}
This doesn’t make much sense when you think about it conceptually.
Shouldn’t a function be its own context and therefore have its own this?
The reason why this is the case is not very complicated: JavaScript was
originally created for novices to make scripting fast. So to make things
easier, it auto-populates a function’s this value with the global this
or window.
That means window is available by default in all of your functions via
this keyword. That’s kind of useful, but it gets messy if you want a function
to have a coherent, separated context.
Sloppy Mode
When we write JavaScript code, by default, all code is usually written in
something called “sloppy mode,” which accommodates for novices by
ignoring some errors and causing functions to inherit the global context.
To exit sloppy mode, we have to switch to something called “strict” mode.
105
Chapter 6 Functions and Classes
Strict mode brings many advantages to your code, the main one
being separating out function contexts from the global context. Both files
and functions can be made strict by adding the “use strict” text at the
top. By putting our code into strict mode, we can give each function its
own context, and thus the keyword this will return undefined inside a
function. In the following example, strict mode is enabled. To enable strict
mode, you just have to add “use strict” to the top of your file:
"use strict"
console.log(this) // Window { }
let words = function (word1, word2) {
console.log(this) // undefined
return word1 + " " + word2
}
"use strict"
console.log(this) // Window { }
let words = () => {
console.log(this)
}
words() // console logs Window { }
106
Chapter 6 Functions and Classes
"use strict"
let contextualFunction = function() {
let words = () => {
console.log(this) // console logs undefined
}
words()
}
contextualFunction() // console logs undefined
107
Chapter 6 Functions and Classes
"use strict"
let words = function (word, punctuation) {
return this.keyword + " " + word + punctuation
}
let wordContext = { keyword: "Hello" }
let helloWorld = words.call(wordContext, "World", "!")
console.log(helloWorld) // "Hello World!"
Figure 6-3. Functions are added to the top of the stack as they are
declared – just like variables
108
Chapter 6 Functions and Classes
If we were calling words() all the time, we’ll find that we will have to
keep mentioning our context over and over again – which is not ideal.
That’s because we need to reference it every time we use call or apply. In
the following example, we want to call words() twice and use the same
context for each call. This means we have to write the same code twice:
"use strict"
let words = function (word, punctuation) {
return this.keyword + " " + word + punctuation
}
let wordContext = {
keyword: "Hello"
}
let helloWorld = words.call(wordContext, "World", "!")
let goodbye = words.call(wordContext, "Goodbye", "!")
console.log(helloWorld) // "Hello World!"
console.log(goodbye) // "Hello Goodbye!"
"use strict"
let words = function (word, punctuation) {
return this.keyword + " " + word + punctuation
}
let wordContext = {
keyword: "Hello"
}
let boundWord = words.bind(wordContext)
109
Chapter 6 Functions and Classes
Note With the new keyword, you can omit the double brackets
when calling your function if you have no arguments. So new
myFunction() is the same as new myFunction.
110
Chapter 6 Functions and Classes
Since they generate their own context, console logging of this will
return myFunction { }, which contains details about the function (under
constructor), and the global prototype for object types since functions are
of type object. You can see what this looks like in Figure 6-4.
Figure 6-4. When using the new keyword, this refers to the function’s
prototype, which contains details on the function, and its inherited
object prototype. This is not super important for writing code from
day to day but is useful to know nonetheless
111
Chapter 6 Functions and Classes
112
Chapter 6 Functions and Classes
else {
return "Hello World"
}
}
User.prototype.giveName = function() {
return `My name is ${this.fullName}!`
}
113
Chapter 6 Functions and Classes
let Animals = {
value: [ 'dog', 'cat' ],
get listAnimals() {
return this.value
},
set newAnimal(name) {
this.value.push(name)
console.log("New animal added: " + name)
}
}
Animals.newAnimal = "sheep"
console.log(Animals.listAnimals)
114
Chapter 6 Functions and Classes
What is interesting about getters and setters is that you don’t call them
using the double brackets like Animals.newAnimal(). Instead, they are
called whenever you check the property name. So Animals.newAnimal
= "sheep" is used rather than Animals.newAnimal("sheep"). Similarly,
Animals.listAnimals will list all animals, without needing to run the
function.
This gives them the appearance of being normal properties, with the
added benefit of being able to run functional code.
Generator Functions
The final type of function we’ll look at is known as the generator function.
Earlier, when we looked at array iteration, we mentioned how we could
create an iterator with access to the next() method by accessing the
iterator protocol:
function* someGenerator(x) {
yield x;
}
const runG = generator(1)
console.log(runG.next()) // {value: 1, done: false}
console.log(runG.next()) // {value: undefined, done: true}
115
Chapter 6 Functions and Classes
will only work once. After that, it will be marked as done as is shown in the
previous example. As such, generator functions remember where you left
off and continue on from that point.
In the following example, we run an infinite loop, which also increases
the value of a variable called index each time. The function remembers
the value of index, each time we run next, allowing us to access the next
calculation in the sequence each time:
function* someGenerator(x) {
let index = 0
while(true) {
yield x * 10 * index
++index
}
}
function* someGenerator(x) {
let index = 0
while(true) {
yield x * 10 * index
return 5
}
}
116
Chapter 6 Functions and Classes
Classes
JavaScript is a prototypical language, and as we’ve seen already,
inheritance occurs via prototypes. Many other languages have classes,
which can make JavaScript seem like quite a departure in syntax.
To alleviate this problem of unfamiliarity, JavaScript implemented
classes. For the most part though, classes in JavaScript are basically just
syntactic sugar for writing constructor functions, with a few additional
capabilities specific to classes.
Classes do not have to be used to write JavaScript since they provide
little in the way of new functionality, but they are becoming more common
as JavaScript inherits more developers who are used to writing class
based software. Classes always have to be called with the new keyword,
and they always run in strict mode, so they create their own context by
default without the need of strict mode.
117
Chapter 6 Functions and Classes
normal functions, and any arguments passed to the class will go into the
constructor function, as is shown in the following example:
Classes can be written in two ways, either where we use a let or const
variable to define the class:
Class Methods
Just like when we declare functions using the new keyword, we can also
define methods on classes. These are defined directly in the class body and
work in the same way. As an example of how this works in practice, let’s
create a class called HotSauce, with a couple properties and a method to
retrieve how hot the sauce is.
118
Chapter 6 Functions and Classes
119
Chapter 6 Functions and Classes
constructor(name, hotness) {
// We can assign arguments from new
instances of
// our class to this as well
this.hotness = hotness
this.name = name
}
getName() {
if(this.hotness < this.maxHotness) {
return `${this.name} is ${this.hotness}
${this.units}`
}
else {
return `${this.name} is too hot!`
}
}
}
120
Chapter 6 Functions and Classes
Sometimes, you don’t want a class editable like this. As such, JavaScript
provides two other types of fields we can use with classes:
–– Static fields, which cannot be accessed on a new
instance of a class itself but only on the original class
(which we’ll look at in more detail soon)
static classDetails() {
return `${this.className} by ${this.author}`
}
}
Instead, we need to call the static method directly on the Utility class
as is shown in the following example:
Since we don’t initiate a new instance of our class, static methods will
also not have access to non-static properties on the class. For example, if
we had not called className and author static in the preceding example,
then they would have been undefined when we tried to reference them in
classDetails().
122
Chapter 6 Functions and Classes
Utility.classDetails = function() {
return "Some return text"
}
123
Chapter 6 Functions and Classes
While all the other features of classes are just features objects already
had, private fields are actually new functionality that are only available via
classes. Private fields are not available on objects in the same way as they
are in classes.
124
Chapter 6 Functions and Classes
prototype of the child class. Consider the following example, using our
HotSauce class:
class HotSauce {
// Fields here are added to this, so they are available
// via this.units and this.maxHotness in methods
units = 'scoville'
maxHotness = 20000000
constructor(name, hotness) {
// We can assign arguments from new instances of
// our class to this as well
this.hotness = hotness
this.name = name
}
getName() {
if(this.hotness < this.maxHotness) {
return `${this.name} is ${this.hotness}
${this.units}`
}
else {
return `${this.name} is too hot!`
}
}
}
From Figure 6-5, you’ll see that the prototype of this class contains
the methods getName() and the constructor. To further explain this
concept, let’s create a new class which extends this, called VeganHotSauce.
This logically makes sense since only some hot sauces are vegan:
125
Chapter 6 Functions and Classes
this.meat = meat
}
checkMeatContent() {
if(this.meat) {
return "this is not vegan"
}
else {
return "no meat detected"
}
}
}
126
Chapter 6 Functions and Classes
As you might expect, both static and private methods are not inherited
when using extends. That is because both, conceptually, belong to the
parent class.
127
Chapter 6 Functions and Classes
checkMeatContent() {
console.log()
if(this.meat) {
return "this is not vegan.. but " + super.getName()
}
else {
return "no meat detected.. and " + super.getName()
}
}
}
128
Chapter 6 Functions and Classes
S
ummary
In this chapter, we’ve covered quite a lot of ground on functions and
classes. We’ve shown you how to create functions and how they inherit
based on prototypical inheritance. We’ve talked about how to give
functions context with the this keyword. We’ve also looked at the many
different ways you can define functions in your code, and how they
differ. Finally, we’ve looked at classes, and explained how they are mostly
syntactic sugar for objects. We’ve gone into detail using examples on how
class inheritance work in JavaScript. While classes can be familiar to some
developers, if you find it hard to understand or unintuitive, it’s still fine to
just work with functions and prototypical inheritance where necessary.
Whether you decide to use classes or functions is mostly stylistic.
In JavaScript, many developers use functions since that’s the way the
language was originally devised. As mentioned before, the most important
thing is that you maintain consistency in your code and that your team
understand what the code you are writing means.
129
CHAPTER 7
Types
In software development, a type refers to a property we apply to a language
construct to describe what the data is about. In this description, a language
construct refers to something like an expression or variable. We’ve
already discussed how JavaScript is both weakly and dynamically typed,
which means we do not have to explicitly declare types on language
constructs. That does not, however, make types in JavaScript any more
straightforward than other languages, and in some ways, it makes things
more complicated! Types are where some of the more unusual quirks of
JavaScript come to roost, so getting a good grasp on them can be tricky.
Since we will be talking about types in this chapter, it’s useful to
understand how to find the type of something. The type of anything can be
found using the typeof keyword:
console.log(typeof 5) // 'number'
console.log(typeof "name") // 'string'
Primitive Types
Primitive types are types which have no methods or properties by default,
and are not objects. These types of data cannot be changed once they are
defined, and in memory terms are stored on the stack. In JavaScript, there
are a total of 7 primitive types, all of which are all listed in Table 7-1. We’ve
seen some of these types already in our code.
let x = 5
string Any string value
For example:
For example:
let x = 1000n
undefined Any undefined data (i.e., a variable with no value)
For example:
let x = undefined
boolean Any true or false value
For example:
let x = true
null A reference that points to a nonexistent address in memory
For example:
let x = null
symbol A guaranteed unique identifier
For example:
let x = Symbol("id")
132
Chapter 7 Types
Primitive Wrappers
By definition, primitives have no methods, but they often behave as though
they do. Every primitive type except for null and undefined are associated
with what are known as wrapper objects, which are referenced every time you
try to call a method on a primitive type. As an example, the wrapper object
for string types is called String. You can find all the methods you can apply to
strings by console logging String.prototype, which can be seen in Figure 7-1.
133
Chapter 7 Types
–– Object
–– Symbol
–– Number
–– String
–– BigInt
–– Boolean
If you ever need to know what methods are available to various types,
you can find them by console logging Wrapper.prototype, where Wrapper
is Object, Symbol, Number, etc.
Calling methods from a wrapper on a primitive type can be done
directly on the primitive itself or on a variable pointing to the primitive.
In the following example, we use one of those methods, .at(), on a string
and on a variable of type string:
(5).toString() // '5'
5..toString() // '5'
134
Chapter 7 Types
Number.toString(5) // '5'
135
Chapter 7 Types
The same works for numbers, which coerces numerical strings into
numbers:
136
Chapter 7 Types
While calling Number() and String() like this creates a new primitive,
calling it as a classic constructor with the new keyword will lead to some
unexpected behavior. For example, new String will create an object
without an accessible primitive value:
137
Chapter 7 Types
The confusion does not stop there. Since we can’t check if something
is NaN by doing NaN === NaN, we have a function called isNaN to do the job
instead:
isNaN(5) // false
isNaN(NaN) // true
138
Chapter 7 Types
To fix this, JavaScript added a new method later on, which does not
coerce data into numbers if it wasn’t a number in the first place. The name
of that method is the same, but it’s found via Number.isNaN(), instead of
isNaN() (or window.isNaN()). This is, to say the least, a little confusing.
So while isNaN("hello") is false since Number("hello") converts to
NaN, Number.isNaN("hello") is false since "hello" is not equal to “NaN.”
As such, Number.isNaN is a much more reliable way to check if something
is NaN since no type coercion is involved – but it only checks for direct
equality to NaN itself.
In summary:
139
Chapter 7 Types
140
Chapter 7 Types
Mathematical Methods
As we’ve mentioned, number types not only have the wrapper type
Number but also a utility global object called Math that contains a bunch
of mathematical constants and methods. While we’ve looked at the
constants already, the methods that exist on this global object are also
quite useful. It’s worth familiarizing yourself with some of these. You can
see some of these in Figure 7-3.
Figure 7-3. If in doubt about an object, always console log it! Console
logging Math (which can be done from your browser’s console log)
shows you all the mathematical constants and methods you can use
141
Chapter 7 Types
142
Chapter 7 Types
As well as these there are log methods, which are listed in the
following, and all geometric methods you would expect, like Math.tan,
Math.sin, Math.atanh, Math.asin, and so on:
143
Chapter 7 Types
Figure 7-4. Calling the Date() constructor will create a string of the
current date and time
Unlike the other wrapper objects we’ve looked at, it’s actually better to
call Date as a constructor since you get a bunch of useful utility methods
along with it:
The various methods for getting dates in JavaScript are shown below:
144
Chapter 7 Types
It’s also worth noting that JavaScript’s Date() constructor can parse
date strings into dates, but since this behavior is not standardized across
browsers, it’s generally not recommended:
If you need to parse a date, it’s better to pass in the year, month, and
day separately – to ensure it works in all browsers:
145
Chapter 7 Types
Alternatively, all the preceding get methods when changed to set allow
you to set a date. For example, getDate() becomes setDate(), to set the
day of the month; getFullYear() becomes setFullYear(), and so on:
The locality defined here is fr-FR, but en-GB or en-US would also be
valid. The two-digit language comes first, followed by a dash, and then
the two-digit country code. These codes follow ISO 3166 and ISO 639,
respectively.
We used toLocaleString() earlier, but toLocaleDateString() and
toLocaleTimeString() can also be used. The difference is one returns the
date while the other returns just the time.
146
Chapter 7 Types
console.log(myObject[idSymbol1]) // "some-id"
console.log(myObject[idSymbol2]) // "some-other-id"
147
Chapter 7 Types
Figure 7-5. Symbols allow for the creation of unique keys which are
unique regardless of their name, allowing you to avoid key conflicts
when adding new items to objects
While the preceding code has allowed us to create unique values using
the Symbol constructor, you can still create unique keys with symbols that
do override each other. Symbol.for() will find a symbol for a specific key
or create one if it is not found. This will mean that you can only create one
symbol “for” a specific key. In our previous example, this would allow the
keys to override each other:
console.log(myObject[idSymbol1]) // "some-other-id"
console.log(myObject[idSymbol2]) // "some-other-id"
148
Chapter 7 Types
let x = 5
if(x > 0 && x < 10) {
console.log("hello world")
}
150
Chapter 7 Types
In the following example, we’re first asking, is x > 0 falsy? If it is, then
return its result. Otherwise, it’s truthy, so check the next statement. If
the next statement is falsy, return it. Otherwise, return the last statement
anyway. So the preceding statement actually returns x < 10, which is true,
and thus the if statement is true:
let x = 5
if(x < 0 && x < 10) {
console.log("hello world")
}
This means that the logical AND operator can be used in variables too.
For example, the following variable myVariable returns “hello world” since
x > 0 is truthy:
Logical OR Operator
We’ve also encountered the logical OR operator before. In actuality, the
OR operator is just the opposite of the logical AND operator. While AND
returns the first falsy value, OR returns the first truthy value instead!
Much like AND, then, it can be used in logical statements and
variables. The difference is that OR will check if the first value is truthy and
return it if it is. Otherwise, if it’s falsy, it will return the next value instead:
151
Chapter 7 Types
let someValue = 5
let x = 0 || "hello world" // "hello world", since 0 is falsy
let y = null || someValue < 0 // false, since null is falsy, it
returns the second value
let z = "hello world" || 0 // "hello world", since "hello
world" is truthy
Nullish Coalescing
What we’re seeing here is that falsy and truthy are a little messy. Since data
is being coerced into either false or true, some unexpected behavior can
occur. For example, falsy can mean all sorts of things, such as 0, undefined,
null, document.all, and NaN.
Long after truthy and falsy established themselves in the language,
JavaScript came up with the idea of nullish coalescing. This is an alternative
to the truthy and falsy behavior seen in logical AND/logical OR operators.
Nullish coalescing, which is indicated by the ?? operator, will only return
the second part of a statement if the first is null or undefined. This confines
checks to two well-defined primitives, making behavior a little more reliable.
In the following code, you can see some examples of how it works in practice:
All of the operators we’ve looked at here, like the logical AND/OR
operators, and nullish coalescing operators can be chained. We’ve looked
at this in previous chapters, but let’s look at how it executes in the code. For
example, we can chain the && operator like so:
Which will ultimately compile to this, since in 0 && false, 0 is the first
falsy result:
let x = 0
Optionality
The final topic we will cover in this chapter is optionality. Optionality is
another way we can control types. Specifically, optionality allows us to
control what happens if an undefined value appears where we didn’t
expect it to.
Let’s consider an example. Imagine we have an API which sends us
data in object form. The object sometimes looks like this:
let userObject = {
"name" : "John",
"age" : "42",
153
Chapter 7 Types
"address" : {
"flatNumber" : "5",
"streetName" : "Highway Avenue"
"zipCode" : "12345"
}
}
But what if the API can also send us data in a slightly different from,
like this?
let userObject = {
"name" : "John",
"age" : "42",
"address" : {
"houseNumber" : "5",
"streetName" : "Highway Avenue"
"locale" : {
"state" : "AZ",
"city" : "Cityopolis"
}
}
}
This will work fine if we get an object with the city in it, but if it’s
missing, then userObject.address.locale.city will return “undefined.”
Worse still, if locale is missing (like in the first object), then JavaScript will
154
Chapter 7 Types
if(userObject?.address?.locale?.city) {
155
Chapter 7 Types
While this is a lot simpler, it will still return “undefined” if any property
in the chain is missing or undefined. That could mean the user would see
the word “undefined”, which is not something we want.
To avoid this scenario, we can use nullish coalescing to respond with
a default value if undefined is returned. This has an additional benefit, in
that it means we can remove the if statement altogether:
Now cityString will return “John is from [[city]]” if a city exists, and
it will return “John is from an unknown city” if it does not. Since we didn’t
need to use an if statement, a cityString is now always available, and not
confined within a block scope, even if the city is undefined. Our final code
with the userObject looks like this:
let userObject = {
"name" : "John",
"age" : "42",
"address" : {
"houseNumber" : "5",
"streetName" : "Highway Avenue"
"locale" : {
"state" : "AZ",
"city" : "Cityopolis"
}
}
}
156
Chapter 7 Types
Summary
In this chapter, we’ve covered types. We’ve looked at primitive types,
objects, and their corresponding wrappers. We’ve explained how
although primitives do not have their own methods or properties, they all
automatically inherit wrappers, which gives them the illusion of having
methods and properties. We’ve also deep dived into some important
types that we haven’t looked at much up until now, like numbers
and dates. We’ve described how JavaScript’s type complexity leads to
157
Chapter 7 Types
interesting problems like truthy and falsy values, and their corresponding
operators. Finally, we look at optionality, and how it can be used to contain
errors when you don’t know that much about a specific object. Types in
JavaScript are easy to get started with, but become more complicated as
you get deeper into the detail. Having a good grasp of how they work is a
requirement for writing good JavaScript code.
158
CHAPTER 8
Manipulating and
Interacting with HTML
So far, we’ve only been looking at how we can create code to perform
certain calculations in JavaScript. At some point, we’re going to want to
convert that into actual user output which people can see on their screens.
Since JavaScript’s main purpose has always been to add interactivity to
web pages, there are many ways to achieve this.
In this chapter, we’ll be looking at how we can use JavaScript and more
specifically the document Web API to create interactivity and actually
affect what the user sees on the web page.
These two objects are really important for front end development: The
window object holds a lot of useful methods and properties on the user and
their current activity, such as the current screen size and scroll position.
The document object, on the other hand, gives us methods and properties
relating to the HTML document itself. For example, document.URL refers to
the URL for the current HTML document.
You can get a feel for what is available on both of these objects by
console logging them in either your console or a script you are writing.
Note Since all window methods are available globally, you can
skip window. For example, window.alert() is a function used
for creating an alert box that the user can’t skip. It can be called as
just alert(). That is also why you can access document via just
document, rather than window.document.
160
Chapter 8 Manipulating and Interacting with HTML
The window object also contains many useful methods and objects,
which we have used already. As mentioned, each of these can be accessed
without the window prefix:
161
Chapter 8 Manipulating and Interacting with HTML
162
Chapter 8 Manipulating and Interacting with HTML
can see an example of what happens when you console log the DOM in
Figure 8-3. Doing this will print out the current HTML page.
<!DOCTYPE HTML>
<html>
<head>
<title>Hello World Page</title>
</head>
<body>
<p>Hello World</p>
</body>
</html>
163
Chapter 8 Manipulating and Interacting with HTML
In CSS, if we wanted to make the paragraph have red text, it’s relatively
straightforward to apply a style which does that:
p {
color: red;
}
164
Chapter 8 Manipulating and Interacting with HTML
<!DOCTYPE HTML>
<html>
<head>
<title>Hello World Page</title>
</head>
<body>
<p>Hello World</p>
<p>Goodbye World</p>
</body>
<script type="text/javascript">
// Our JavaScript
</script>
</html>
Since HTML loads from top to bottom, the location of our JavaScript
actually matters. If we put JavaScript at the end of our HTML document,
as we have done here, then the DOM will have already loaded by the time
the script runs. If the DOM is loaded, we can use methods like document.
querySelector() without any concerns.
If you put your JavaScript in the head of the HTML document instead,
however, the DOM will not have loaded yet. That means trying to select
HTML elements will not work.
In these scenarios, you need to tell JavaScript to wait for the DOM to load.
To do that, we need to add what is known as an “event listener,” which listens
for specific events, to the HTML DOM (or document object) itself. This event
listener will “fire” whenever the DOM is loaded, meaning you can only use
methods like document.querySelector() within the event listener body:
165
Chapter 8 Manipulating and Interacting with HTML
<head>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", (e) => {
// JavaScript goes here…
})
</script>
</head>
<!DOCTYPE HTML>
<html>
<head>
<title>Hello World Page</title>
</head>
<body>
<p class="hello-paragraph">Hello World</p>
<p id="goodbye">Goodbye World</p>
</body>
<script type="text/javascript">
let allParagraphs = document.querySelectorAll("p")
</script>
</html>
166
Chapter 8 Manipulating and Interacting with HTML
• document.getElementByClassName() returns an
HTMLCollection of HTMLElements.
• document.getElementByTagName() returns an
HTMLCollection of HTMLElements.
In simple terms, these are sort of like arrays, but since they do
not inherit Array.prototype, they lack all of the standard methods
that arrays have. They instead inherit from NodeList.prototype and
HTMLCollection.prototype. As usual, console logging either of these will
tell you all of the methods available for each as is shown in Figure 8-4.
167
Chapter 8 Manipulating and Interacting with HTML
168
Chapter 8 Manipulating and Interacting with HTML
Either way means we can then iterate over our Array using any array
methods, like forEach, or by using a for method like for...of:
Note You may have heard of libraries and frameworks like jQuery,
Angular, React, and Vue. These are libraries that make it easier
to create and interact with web pages via JavaScript. All of these
libraries build off the fundamentals we are covering here.
169
Chapter 8 Manipulating and Interacting with HTML
Note forEach loops are typically slower than for loops. If you can
use for loop, it’s always recommended.
If you only wanted to affect the first paragraph on a page, then you
can use querySelector("p"). In this case, you don’t need to loop through
each item, as only one will be returned.
170
Chapter 8 Manipulating and Interacting with HTML
console.log(HTMLElement.prototype)
All HTML DOM elements also inherit from another wrapper object
called Element.prototype. The additional methods inherited from
Element.prototype can also be console logged. You can see the output for
both of these prototypes in Figure 8-6.
console.log(Element.prototype)
171
Chapter 8 Manipulating and Interacting with HTML
So far we’ve only looked at generic queries to select HTML elements
using methods like querySelectorAll, but we can also select elements
based on their specific ID using getElementById too. This will return
only one element since IDs in HTML should be unique. If your HTML
document contains more than one tag with the same ID by accident, then
getElementById will only return the first element. This method is slightly
faster, so it’s preferable where possible to use.
In the following example, we style all paragraphs to have the text color
red, and then we style any with the ID “goodbye” in green.
Since goodbyeParagraph is still a paragraph, the first part of our code
will set its color to red initially. Then, when we apply the color green to it,
it will overwrite red – resulting in one red paragraph and another in green.
You can see the outcome of this in Figure 8-7.
HTML:
JavaScript:
goodbyeParagraph.style.color = "green"
Figure 8-7. How our HTML page looks after applying CSS styles via
JavaScript to allParagraphs and goodbyeParagraph
172
Chapter 8 Manipulating and Interacting with HTML
173
Chapter 8 Manipulating and Interacting with HTML
<!DOCTYPE HTML>
<html>
<head>
<title>Hello World Page</title>
<style>
#main-text {
opacity: 0;
}
</style>
</head>
<body>
<button id="click-me">Click Me</button>
<p id="main-text">Main Text</p>
</body>
</html>
While this page has no interactivity by default, we can change that with
JavaScript. You’ll notice that in our CSS, #main-text is hidden by setting its
opacity to 0. Let’s write some JavaScript so that if the user clicks the button,
"#main-text" will have it’s opacity adjusted to 1, so that it appears:
Using this code, whenever a user clicks a button with the ID “click-me,”
we fire off a function that sets the opacity of “main-text” to 1. This has one
problem, though: it only works once! When the user clicks the button, it
can only set the opacity of “Main Text” to 1. What if we want it to appear or
hide every time the button is clicked?
174
Chapter 8 Manipulating and Interacting with HTML
To do that, we can use CSS classes, and luckily all HTMLElements have
methods to alter CSS classes. These methods are found on HTMLElement.
classList. Let’s first change the HTML layout so that we have a separate
class for hidden elements with opacity values of 0. With a setup like this,
we don’t need to rely on adding styles directly to the element #main-text
anymore. Instead, we can change the class which the HTML element has:
<!DOCTYPE HTML>
<html>
<head>
<title>Hello World Page</title>
<style>
.hidden {
opacity: 0;
}
</style>
</head>
<body>
<button id="click-me">Click Me</button>
<p id="main-text" class="hidden">Main Text</p>
</body>
</html>
175
Chapter 8 Manipulating and Interacting with HTML
• HTMLElement.classList.contains("class") – For
checking if an HTML element contains a certain class
• HTMLElement.classList.remove("class") – For
removing a class from an element
• HTMLElement.classList.replace("oldClass",
"newClass") – For replacing a class with a new class
Creating a Counter
To further our understanding of how we can interact with the HTML
DOM, let’s look at another example. In this simple example we will create
a counter which increases every time the user clicks a button. Let’s start
by creating an HTML page that contains both our counter and a button to
increase it:
<div id="counter">0</div>
<button id="plus-button">
Click Me
</button>
Next let’s create the JavaScript we’ll need to make this work. In the
following example, we check for click events on the “plus-button” element.
When one occurs, we take the text value from “counter” and parse it into
a number using a method we’ve covered before, called parseInt. We add 1
to that number and then show that new number to the user by inserting it
176
Chapter 8 Manipulating and Interacting with HTML
into the counter element. This is illustrated in the following code, and you
can also seehow this looks in Figure 8-8.
button.addEventListener("click", function(e) {
let counter = document.getElementById("counter")
let counterValue = parseFloat(counter.textContent)
counter.textContent = counterValue + 1
})
Event Types
In the previous examples, we’ve only looked at the click event, but there
are loads of other useful events in JavaScript too. The following is a list
of the events most important to HTMLElements, and the ones you’ll find
yourself using the most.
177
Chapter 8 Manipulating and Interacting with HTML
178
Chapter 8 Manipulating and Interacting with HTML
This allowed us to do some neat things which click could not achieve.
For example, we could store information on when the user pressed their
mouse down and then do something if they continued to move it. In the
following example, we can test if the mouse is down by using the mouseup
and mousedown event listeners and a variable that stores that state. Then we
can check on this variable to do something specific if the user moves their
mouse while it is clicked:
179
Chapter 8 Manipulating and Interacting with HTML
})
document.addEventListener("mousemove", function(e) {
if(isMouseDown) {
console.log("The user is dragging!")
}
})
document.addEventListener("mouseup", function(e) {
isMouseDown = true
})
This kind of setup was most frequently seen when trying to add drag
and drop to web pages. When touch devices came, it complicated things. We
couldn’t really remove mouse events, but we needed new ones for touch. So
JavaScript added touch equivalents: touchdown, touchmove, and touchup.
As device user experience began to merge across both mouse based
devices and touch devices, and code bases became more and more
complicated, having these two separate events became a bit of a nuisance.
To remedy this final problem, JavaScript created three new events:
pointerdown, pointermove and pointerup. These fire for both touch and
mouse events, meaning there is no need to run both events separately.
The reason we have three is historical, but it is also sometimes useful.
For example, we can detect if someone is interacting via a touch device,
just by checking for the touchdown event firing:
document.addEventListener("touchdown", function(e) {
// Touch device
})
180
Chapter 8 Manipulating and Interacting with HTML
<div id="my-div">
<div class="some-div">
<p class="paragraph">Hello World</p>
</div>
</div>
181
Chapter 8 Manipulating and Interacting with HTML
document.getElementById("my-div").addEventListener("click"...
Figure 8-10. Some events will bubble up to the next element in the
DOM by default. Clicking #my-div also counts as a click on body and
document
182
Chapter 8 Manipulating and Interacting with HTML
document.addEventListener("click", function(e) {
e.stopPropagation()
})
Capturing Phase
We can force events to fire via capturing instead of bubbling. This
ultimately has the impact of changing the order in which events occur.
Consider again our example from earlier:
183
Chapter 8 Manipulating and Interacting with HTML
<div id="my-div">
<div class="some-div">
<p class="paragraph">Hello World</p>
</div>
</div>
document.getElementById("my-div").addEventListener("click",
someEvent)
document.getElementById("my-div").addEventListener("click",
someEvent, { capture: true })
Using this code, the same thing happens from a user perspective, but
it’s subtly different in terms of code execution:
184
Chapter 8 Manipulating and Interacting with HTML
Setting an event to capture is not really used a lot, but it can come in
useful in some scenarios, where we want events to fire slightly earlier than
they otherwise would, since events applied in the capture phase always
happen before those applied via bubbling.
}, {
capture: true,
once: false,
passive: true
})
Along with capture, which we have already covered, there are two
other options:
The first two options are straightforward given what we’ve discussed
already, but the last requires some explanation.
185
Chapter 8 Manipulating and Interacting with HTML
for(let x of anchors) {
x.addEventListener('click', (e) => {
e.preventDefault()
})
}
Another use can be disabling certain keys. You could do this by using
e.code, which contains the key clicked for keyboard events:
document.getElementById("form-input").
addEventListener("keydown", function(e) {
if(e.code = "KeyE") {
console.log("Don't press the e key!")
e.preventDefault()
}
})
This works since JavaScript events come before the actual default
behavior of inputs. That means your code fires before the actual key is
shown on the screen.
Now, let’s go back to the passive option. The main use case for setting
passive to true on an event is to improve scrolling for wheel, touchmove,
touchstart, and mousewheel events. Beyond this, it does not have much
186
Chapter 8 Manipulating and Interacting with HTML
utility. Most browsers even do this by default now, but Safari does not. As
such, you will need to enable passive for these events to ensure they work
smoothly in Safari.
The reason this improves scrolling is because all browsers have features
built in to ensure smooth scrolling. Events like wheel, which also causes
scrolling, interfere with this and thus have much reduced performance
when compared to scroll. By promising JavaScript that we won’t prevent
default scrolling behavior on these events, JavaScript is freed up to process
the scrolling immediately – resulting in much smoother scrolling.
187
Chapter 8 Manipulating and Interacting with HTML
window.addEventListener("resize", function(e) {
let getHeader = document.getElementById("top-banner")
console.log(`The screen width is ${window.innerWidth}px`)
if(window.innerWidth < 700) {
getHeader.classList.add("small")
}
})
This can be used more simply to gain live data on screen or window
size, as is shown in the following example. You can see the result of this
code in Figure 8-11.
188
Chapter 8 Manipulating and Interacting with HTML
We can use the same concept to capture live data on the user’s mouse
position. This is really important in use cases like drag-and-drop. If we
want to know the live position of the user’s mouse cursor or finger in a
touch setting, we have to use an event since the window and document
properties only hold information on what the state was when the page
189
Chapter 8 Manipulating and Interacting with HTML
loaded. Code like that in the following will contain live information on a
user’s finger or mouse position as they move it. Any movement will result
in a new console line, just like any resize did in Figure 8-11.
document.body.addEventListener("pointermove", function(e) {
console.log(`The user's position is X: ${e.pageX}px Y: ${e.
pageY}px`)
})
<div id="drag"></div>
#drag {
background: red;
width: 70px;
height: 70px;
position: relative;
margin: 20px;
left: 0;
top: 0;
color: white;
}
190
Chapter 8 Manipulating and Interacting with HTML
We will utilize two new functions we haven’t seen before to make this
work. These two functions, getComputedStyle and getPropertyValue, will
let us retrieve the current CSS properties for a given HTML element.
Let’s start by creating the bare bones we’ll need to add dragging to the
div we’ve created earlier. The comments in the following example should
also help you understand what is going on:
Note If you’re not familiar with CSS, the left and top properties
allow you to move an element by a certain amount from its original
location. Both left and top push the element from the left and top by a
certain amount, respectively.
Now let’s add the actual drag event. For this to work, we need to
calculate how much the user has moved their mouse by subtracting their
initial mouse position from wherever they’ve moved to. We will only do
this if the dragging variable is true, which implies the user has clicked
their mouse down on the draggable div.
All we need to do now is add this modification value to the left and top
values in initialBlockPosition and set the <div>’s CSS to match the new
value. We can then apply this number as a CSS left and top value to move
192
Chapter 8 Manipulating and Interacting with HTML
the div. I’ve also updated the element’s innerHTML so that it contains
information on the <div>’s current coordinates. The code for this is shown
in the following example, and can also be seen in Figure 8-12.
Figure 8-12. Using the tools we’ve learned about so far, we can add
drag-and-drop functionality to our web pages by calculating the
position of a <div> element
193
Chapter 8 Manipulating and Interacting with HTML
The e variable
We’ve touched on a lot of important concepts around event listening
and DOM manipulation. In these examples, we’ve used methods from a
variable called e, which is available to all events when we called our event
listener function:
getButton.addEventListener("click", function(e) {
document.body.addEventListener("click", function(e) {
if(e.pageX > 500 && e.pageY > 200) {
console.log("Hello World!")
}
})
document.getElementById("form-input").
addEventListener("keydown", function(e) {
console.log(`You pressed a key with the key code ${e.code}`)
})
Since e is just a variable, you can console log it too. You can see an
example of the e variable for a click event in Figure 8-13.
194
Chapter 8 Manipulating and Interacting with HTML
Figure 8-13. The e variable is invaluable for events and can tell you
all sorts of useful information about what the user did in the event.
Here is an example for a click event
195
Chapter 8 Manipulating and Interacting with HTML
We can make any element this way. Just change "div" in the following
example to the kind of element you want to make. Once we initiate the
creation of an element, we need to use some other methods and properties
to define how that element should be. In the following example, along with
using style and classList, we also use setAttribute to create a custom
attribute for newEl:
newEl.style.color = "red"
// Let's add a class called 'active' to it
newEl.classList.add("active")
// Let's add an attribute called 'data-attribute'
newEl.setAttribute("data-attribute", true)
196
Chapter 8 Manipulating and Interacting with HTML
While this has allowed us to make an HTML element, it still only exists
in JavaScript! So how do we actually add it to our HTML page? To do that,
we need to use a few methods that exist on all HTMLElements.
You can only really insert new HTML in JavaScript in relation to other
HTML. The methods and properties we have at our disposal to do that are
as follows:
<div id="my-div">
<div class="some-div">
Some Text
</div>
<p class="para">Hello World</p>
<p class="para-2">Hello World</p>
</div>
197
Chapter 8 Manipulating and Interacting with HTML
newEl.style.color = "red"
// Let's add a class called 'active' to it
newEl.classList.add("active")
// Let's add an attribute called 'data-attribute'
newEl.setAttribute("data-attribute", true)
// Let's add some text inside the div
newEl.textContent = "I am a created element."
// Let's give our element an ID
newEl.id = "master-element"
We can insert this new element, newEl, into the HTML document via
reference to another HTML element. For example, we can prepend newEl
to the beginning of #my-div:
After doing this, if we look at our HTML we will see something like this:
<div id="my-div">
<div id="master-element" data-attribute="true"
style="color: red;" class="active">I am a created element</div>
<div class="some-div">
Some Text
</div>
<p class="para">Hello World</p>
<p class="para-2">Hello World</p>
</div>
The append method works in the same way, only it adds the element to
the end rather than the start of an element. If we want to get more specific
198
Chapter 8 Manipulating and Interacting with HTML
about where we insert elements, rather than just adding it within another
element at the start or end, we need to use before and after. For example,
the following code would insert newEl directly after the .para element:
myEl.style.position = "relative"
myEl.style.left = "500px"
myEl.style.marginLeft = "10px"
myEl.style.color = "red"
myEl.style.width = "500px"
myEl.style.height = "500px"
myEl.style.padding = "0 0 0 10px"
199
Chapter 8 Manipulating and Interacting with HTML
It’s also worth noting that another method called deleteRule() exists,
which deletes a certain block of CSS from a style sheet at a certain index.
For example, deleteRule(0) would delete the first block of CSS found in a
stylesheet.
200
Chapter 8 Manipulating and Interacting with HTML
document – in other words, the <html> tag, and is similar to how we can
select the body of an HTML document using document.body.
Since the HTML tag itself is also an HTML element, we can add CSS
variables directly to it using a style method we have not covered before –
setProperty. We do it this way by adding the variables to the HTML tag itself so
that they are available at the root of the CSS document for any other CSS to use.
Using setProperty on the document element, we can create CSS
variables, as is shown in the following example:
document.documentElement.style.setProperty('--my-background-
color', 'red')
document.documentElement.style.setProperty('--my-text-color',
'white')
201
Chapter 8 Manipulating and Interacting with HTML
This means we can get the current location of any HTML element on
demand. The getBoundingClientRect() function can also be used to get
information on the top, left, bottom, right, x, y, width, and height of
any HTMLElement. In the following example, we are able to get the width, x
position, and bottom position of “drag”:
202
Chapter 8 Manipulating and Interacting with HTML
Summary
In this chapter, we’ve covered how to start working with HTML in
JavaScript. We’ve discussed how to select HTML elements using a variety
of different methods and then how to manipulate those elements using
some other HTML specific methods. We then covered user events and how
to track them so that you can do things when a user interacts with your
web page.
We’ve looked at the different wrapper objects assigned to HTML
elements and collections, like HTMLElement, Element, HTMLCollection,
and NodeList. We have also discussed how you can find out more about
the methods and properties available to you via the console log by
using console.log(HTMLElement.prototype). You can also learn more
online about these prototypes and methods via websites like developer.
mozilla.org.
203
Chapter 8 Manipulating and Interacting with HTML
204
CHAPTER 9
Sets
Sets are mutable lists of values, which are quite similar to arrays. The
difference between sets and arrays is that sets cannot contain duplicates.
This makes them very performant for tasks where you need to have a
unique list. If you try to add a duplicate item to a set, it will automatically
be rejected without throwing an error.
Creating a set is relatively simple. All you need is the constructor,
Set(), and then special methods like add and remove to modify it:
Note If you try to add duplicate values to a set, only one will be
added. Sets automatically reject duplicate values.
Sets will only support the addition of unique values for primitives,
but with objects, they use an object’s reference instead of its value. That
means it is possible to add two objects which have the same value but have
different references. This is shown in the following example:
Sets also handle equality in a slightly different way than we’ve looked
at so far. Sets will consider NaN to be equal to NaN even though in previous
examples we should that NaN === NaN and NaN == NaN returns false.
The reason why sets work this way is because they use a slightly different
equality algorithm. These equality algorithms are summarized below:
206
Chapter 9 Maps and Sets
As such, if we try to add two NaNs to a set, the result is that only one
gets added:
// Set(1) {NaN}
console.log(mySet)
Note Sets are really useful when you have an array where
duplicates are not allowed. It’s always possible for faulty code to let
duplicates into an array which should not have any, but sets ensure
that this will never happen.
Modifying Sets
Sets are mutable like other objects, and we mutate them via special
set methods. The three main methods for changing a set are:
207
Chapter 9 Maps and Sets
mySet.clear()
console.log(mySet) // Set(0) {}
Merging Sets
Since sets are iterable and implement the iterator protocol, they can
be merged using the three dots operator (...), as shown in the following
208
Chapter 9 Maps and Sets
Figure 9-1. Sets can be merged using the three dots syntax since
they are iterable. By doing so, the new set will have all duplicates
removed from it
209
Chapter 9 Maps and Sets
mySet.add(6)
for(let x of mySet) {
console.log(x) // 4, 5, 6
}
for(let x in mySet) {
console.log(x) // undefined
}
Just like arrays, sets also have a forEach method built in for easy
iteration. As we’ve previously discussed, for loops are generally faster
than using the forEach method, but they do have some advantages over a
for loop such as creating a new context, since they use callback functions:
mySet.forEach((x) => {
console.log(x) // 4, 5, 6
})
210
Chapter 9 Maps and Sets
SetIterators are different from sets, and they only have one method,
that being the method next(). The next() method allows you to iterate
through sets one item at a time. Each time you do, an object is returned
consisting of the value of the set item and whether iteration is complete.
You can see what that looks like in the following example:
211
Chapter 9 Maps and Sets
for(let x of setEntries) {
console.log(x)
// Returns [ 5, 5 ], [ 10, 10 ]
}
Maps
While sets are array-like structures that also provide automatic
duplicate removal, maps are object-like structures with some additional
enhancements. While similar to objects, they also differ in some very
important ways:
212
Chapter 9 Maps and Sets
And just like sets, maps come with add(), delete(), and clear()
methods. The key difference here is that to add map items, you need to
specify both a key and a value and to delete a map item, you have to specify
the key name you wish to delete. You can see how these methods work in
the following example and in Figure 9-2.
213
Chapter 9 Maps and Sets
myMap.delete("key")
console.log(myMap) // Map(1) {'secondKey' => 'value'}
myMap.clear()
console.log(myMap) // Map(0) {}
Figure 9-2. Adding an entry to the Map using square bracket notation
adds it to the object, at the same level as the prototype. It does not add
an entry to the map itself, meaning you lose all the benefits of Maps
Since it is not possible to set one key before another with set(), maps
order is reliably assumed to be chronological.
Somewhat confusingly, maps seem to be able to have items set using
the square bracket notation, but this is not the right way to set new map
entries. In fact, setting a property on a map using square brackets results in
the property being defined on the map object, and not in the map itself:
console.log(myMap)
The separation between map entries set with the square bracket
notation and the object containing the map is why maps do not inherit
properties from a prototype. You can see this separation in Figure 9-3,
where an [[Entries]] section exists to contain actual map entries, while
the prototype and other properties are set on the map object itself.
214
Chapter 9 Maps and Sets
Figure 9-3. Maps do not inherit a prototype since their properties are
contained in a special property called [[Entries]]. Trying to set a
property on the map itself with square brackets will not work since the
property will be set in the map object
console.log(myMap.get("key"))
215
Chapter 9 Maps and Sets
Since maps do not inherit prototypes, we do not run into key conflicts.
For example, objects will typically inherit keys like valueOf() from their
prototype. That means that we can use this method on any object, as is
shown in the following example:
With maps, these methods are not inherited and thus will not exist.
This prevents any chance of key conflict.
Since maps can have keys of any type, retrieving nonprimitive keys can
become a little tricky. To do that, we must mention the original reference to
the key, meaning we need to store the key in a variable to reliably retrieve
it. You can see how that works in the following example:
myMap.get(someArray) // value
The functionality that allows us to set map keys to any value leads to
some interesting problems. Since the map holds a reference to the object,
someArray, someArray will never be garbage collected. Normally, when
objects become unreferenced, JavaScript will automatically remove them
from memory.
216
Chapter 9 Maps and Sets
217
Chapter 9 Maps and Sets
218
Chapter 9 Maps and Sets
With maps, things are simpler. We can iterate straight on the map itself.
When we do this with a for...of loop, this returns a handy key–value
array item. We also have the option of using the built-in forEach method:
myMap.forEach((x) => {
console.log(x) // 'value', 'value'
})
219
Chapter 9 Maps and Sets
The same functionality we have shown above for keys can be achieved
for values too, using Map.values():
Finally, we can retrieve both key and value in this format by using
Map.entries(). It returns each key–value pair as [ key, value ]:
220
Chapter 9 Maps and Sets
console.log(JSON.stringify(myMap)) // "{}"
The only way to serialize a map for transfer over an API is to convert it
to an object or array. To do that, we can use Array.from() to convert our
map to an array and then use JSON.stringify() to serialize it:
221
Chapter 9 Maps and Sets
Summary
In this chapter, we’ve covered sets and maps. We’ve gone over the utility
of both and when you would use them over regular objects. Sets allow for
unique lists, while maps have lots of extra utility that objects simply do not.
Up until this point, we’ve been relying entirely on objects and arrays
to handle our data. Now you can be more specific and create sets and
maps when the need arises too. Since these objects are optimized for the
tasks we’ve described in this chapter, they can also provide additional
optimization to your code.
222
CHAPTER 10
224
Chapter 10 Fetching Data, APIs, and Promises
225
Chapter 10 Fetching Data, APIs, and Promises
Understanding HTTP
Before we dig into the practicalities of creating APIs in JavaScript, let’s
learn more about HTTP. HTTP, and more commonly HTTPS, the S
standing for secure, is the standard protocol for loading web pages on the
Internet.
You can find out a little more about HTTP if you open your browser’s
console Network tab and then open any web page. This section will
become populated with a lot of entries naming the things that this web
page has loaded via HTTP. If you click one, you’ll see something similar to
Figure 10-2.
226
Chapter 10 Fetching Data, APIs, and Promises
Figure 10-2. Web pages are loaded via HTTP GET requests
227
Chapter 10 Fetching Data, APIs, and Promises
To expose how simple an HTTP request can be, here is the HTTP
request for a made-up website called some-website.org, from a browser
with the user agent Mozilla/5.0. User agent here refers to the unique
string which identifies each browser. In simple terms, all this is doing
is creating a request with some specific instructions which a server will
then interpret. If the server is set up to accept HTTP requests, it will send a
response:
Note To get your own IP and domain, you have to use a web host.
A web host will usually give you a server with a specific IP and a
domain. There are plenty of options for this available online. You can
then host .html, .css and .js files directly on this web server or
use it as a location to spin up a Node.JS web server instead.
RESTful Design
Earlier, we described how most APIs are based around RESTful design
principles. APIs are also usually designed to request specific resources. In
the context of APIs, a resource could refer to anything. For example, in the
context of web development, this could refer to an image, article, category
of content, or comment on an article.
These concepts also extend to navigation on the web itself, where the web
page itself is the resource, and the browser performs a request to retrieve its
contents. Each time an HTTP request is sent, it comes with a specific action
keyword. For example, opening a web page performs the GET HTTP method.
228
Chapter 10 Fetching Data, APIs, and Promises
While a web page (or resource) is normally retrieved via a GET request,
using DELETE on it instead will usually result in the resource being deleted
instead. We usually add authentication to these more dangerous methods
so that users can’t randomly delete important content.
Note In the context of API design, you may see the acronym
CRUD. This stands for Create, Read, Update, Delete, which are the
actions a RESTful API may perform on a resource. These actions are
implemented via POST, GET, PUT/PATCH, and DELETE, respectively.
229
Chapter 10 Fetching Data, APIs, and Promises
230
Chapter 10 Fetching Data, APIs, and Promises
To start to understand how APIs work, let’s create a Node.js web server.
Before you begin, make sure to install Node.js from nodejs.org. This will
also include access to npmjs.org and the npm terminal tools. For the
purposes of this exercise, you can make a new folder anywhere on your
computer and call it “node-server.”
Start by creating a file called app.js in that folder with the following
content:
server.listen(3000)
231
Chapter 10 Fetching Data, APIs, and Promises
npm i http
npm i express
232
Chapter 10 Fetching Data, APIs, and Promises
Now that we’ve installed our packages, let’s look at the rest of the code
found in app.js. First, we use express to create the available URLs on the
web server:
233
Chapter 10 Fetching Data, APIs, and Promises
we can use to send something back to the client via the response.send()
method. If the user navigates to “/some/url” on this server, then the
response they receive will be whatever we define in this send function.
If we plan to send HTML to the user, we can change our response to
do that. This is used in something known as server-side rendering (SSR),
which is a very efficient way to serve HTML to users. Sending HTML files
can be done with the sendFile method in express, as shown in the
following example, where we send a file called “index.html”:
Express supports all of the HTTP methods we described earlier, and while
we’ve only sent responses here, we could also configure these endpoints to do
something else with the request data, like store it in a database.
234
Chapter 10 Fetching Data, APIs, and Promises
node app.js
Note When running node app.js, the app.js file will only run
as long as the terminal session persists. To ensure a web server
continuously runs for as long as the computer or server is switched
on, it’s pretty common to install a production command instead. One
commonly used production command is pm2. You can install it by
running npm install pm2 -g and then permanently start up your
web server by running pm2 start app.js.
235
Chapter 10 Fetching Data, APIs, and Promises
Figure 10-4. Postman lets you test out any endpoint responses from
any public URL. When our web server is running on port 3000, it lets
us easily test responses from it
236
Chapter 10 Fetching Data, APIs, and Promises
if a user clicks “Login,” an API could send their login credentials for
verification to the server. This user-facilitated API call can be done via
.addEventListener(), which we covered in a previous chapter.
237
Chapter 10 Fetching Data, APIs, and Promises
This is not the only way servers and APIs can be used in web
development. Since web browsers trigger a GET HTTP request to any URL
or endpoint you visit, some servers are configured to serve entire web
pages to the user. For example, configuring the endpoint GET /home on a
Node.js server could send a response to the user where the response is the
HTML for the home page.
{
"name" : "John",
"age" : 105,
"interests" : [ "hiking", "skiing", "working" ]
}
To work with JSON, a top-level global object called JSON exists. This
contains two static methods for managing JSON:
238
Chapter 10 Fetching Data, APIs, and Promises
JSON.stringify({
"name" : "John",
"age" : 105,
"interests" : [ "hiking", "skiing", "working" ]
})
/*
This will return a string:
'{"name":"John","age":105,"interests":["hiking","skiing",
"working"]}'
*/
JSON.parse('{"name":"John","age":105,"interests":["hiking",
"skiing","working"]}')
These methods are typically used before we send data to an API and
then when we receive it. For example, before we send JSON data to an
endpoint on a server, we would typically stringify it. Then, on the server
side, we’d parse it to get the JavaScript JSON object back again.
239
Chapter 10 Fetching Data, APIs, and Promises
fetch("https://google.com").then((res) => {
// Do something with response
})
240
Chapter 10 Fetching Data, APIs, and Promises
The reason why is simple. When we try to request a URL, it takes some
time for the URL to load, run the server code it needs to, and then send a
response back to us. Since JavaScript works via the stack, the fetch function
will be run, but JavaScript will just move on to the next item in the stack
and never wait for a response.
For this reason, the fetch function returns a special type of data which
is known as a Promise. It’s called that because fetch() is promising to us
that a response will eventually be generated. When we use then(), it waits
for the promise to generate a response. When it does, it will run the code
inside the then() function’s body. The response from the Promise is stored
in the res variable. In the previous example, we used then() on a fetch
request, but it can actually be used on any Promise.
If you try to console log the res variable, you’ll eventually get the
response object containing data about the response from the server. The
res.body property contains the main body of the response sent by the
server, while res contains other details about the response. The response is
of type ReadableStream, which is the standard transferable object type in
JavaScript.
In this ReadableStream form, it’s not very useful to us, so to convert our
response into usable data, we have five methods attached to every HTTP
response:
• res.text() – Takes the HTTP response body and
returns the text content of its response. If it’s a website,
for example, it returns the HTML.
241
Chapter 10 Fetching Data, APIs, and Promises
To illustrate this, imagine our API returns HTML data. We can use
res.text() to change our body from a ReadableStream to actual,
usable HTML:
fetch("https://google.com").then((res) => {
// this parses the response as text
return res.text()
}).then((data) => {
// we can now access the raw HTML of the web page via data
})
fetch("http://localhost:3000/some/api").then((res) => {
// when fetch is received, jsonObject will become the
parsed JSON version of the response.
return res.json()
}).then((data) => {
// Once received and parsed, we can now access our response
as JSON.
let jsonData = data
})
In summary, we can request any URL or API using the fetch function
in JavaScript. This function returns a special type of data known as a
“promise”. We can wait for a promise to resolve into a response by using a
function called “then”. Now that we understand these basics, let’s look at
how we can customize our fetch requests.
242
Chapter 10 Fetching Data, APIs, and Promises
fetch("https://google.com/", {
body: JSON.stringify({ someData: "value" })
mode: 'cors',
method: 'POST',
cache: 'no-cache',
credentials: 'same-origin',
referrer: 'some-site.com',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
integrity: 'sha512-12345',
keepalive: false,
referrerPolicy: 'no-referrer'
});
243
Chapter 10 Fetching Data, APIs, and Promises
body
The body property is the main data you send to the web server. For
example, we might send a stringified JSON object to the server via
the body. Then, on the server, the body is parsed in order to do some
calculations with it. In the previous example, we saw how we could send
data via the fetch function in the body property. You can see what the
corresponding server code might look like in the following example:
mode
This property is used for configuring how cross-origin requests are
handled. The possible options for this property are cors, no-cors, same-
origin, navigate or websocket. Ultimately, this property will determine
if cross-origin requests (i.e., requests between two different domains) are
successful. If you try to send a request to a different domain (e.g., you are
on localhost, and try to send a request to google.com), then you will get an
error unless you set this to no-cors.
method
We have already discussed HTTP methods throughout this chapter. This
property is where you can set the HTTP method you want to use. It can
be set to POST, GET, DELETE, PUT, CONNECT, PATCH, TRACE or OPTION.
244
Chapter 10 Fetching Data, APIs, and Promises
cache
This property handles how caching works for this fetch request. It can
be set to default, no-cache, reload, force-cache or only-if-cached.
When you make an HTTP request, it is cached and reused to save time. By
changing this setting to force-cache or another setting, you can change the
default behavior so that the cache is never used.
credentials
Especially when configuring login systems, it is possible that the server
will send a cookie back to you to store a session. Cookies are sensitive
information, so it is useful to have a property which configures if the
cookies are visible on the response. By changing this to include, the cookie
will be saved. The possible values for this property are include, same-
origin or omit.
headers
Headers are additional data points about the HTTP request which can
tell the server to do specific things. Some headers are standard, and
servers will interpret them in a specific way – like Cache-Control or
Expires. which control aspects of caching. Other headers are custom. You
can make any header you like, and it can add additional context about the
kind of request you are trying to send.
245
Chapter 10 Fetching Data, APIs, and Promises
redirect
This property determines what happens if the fetched URL redirects.
It can be set to follow, error or manual.
follow is the default value, and error will result in an HTTP
error code. if you use manual, it will result in the HTTP response having a
property called “type” which will be set to opaqueredirect, (res.type =
“opaqueredirect”), allowing you to manually process what should
happen on the client side.
referrer
When you send a request, the response will have a referrer header. By
default, the referrer header is the URL you sent the request from. You can
set a custom referrer via this property, as long as it’s on the same domain
as the fetch request. You can also set it as a blank string to show nothing.
referrerPolicy
Since the referrer is the URL you sent the request from, it can sometimes
make sense to hide this information for security reasons. The
referrerPolicy determines how much referrer information is passed
with the request. Usually, you won’t need to use this, but if you want to
remove the referrer from the response for security reasons, this can be
quite useful.
It can be set to one of the following:
246
Chapter 10 Fetching Data, APIs, and Promises
integrity
This special property is used to validate a subresource integrity (SRI).
For example, a URL can be given a SRI value, which conforms to some
cryptographic standard like SHA-256, SHA-384, or SHA-512. When we
request it via fetch, we can set integrity to the expected SRI value for the
resource we are requesting. If they match, it works. If it does not, then
you’ll get an error instead.
As an example, if you knew that the resource had an SRI value of 12345,
and it had been encrypted with SHA-512, you would set integrity like so:
{ integrity: 'sha512-12345' }
keepalive
When you close a page, all requests are automatically aborted. Sometimes,
this is not helpful. For example, if we were making an analytics tool that
recorded precisely how long a page was open for, we’d never get that data
247
Chapter 10 Fetching Data, APIs, and Promises
back to the server. By setting keepalive, we can create a fetch request that
will outlive the page’s life cycle. The major limitation to this is you will not
be able to process the response, and you can only fetch up to 64kb.
In summary, a fetch request can either be quite simple (requesting a
single page via GET), or it can be made quite complicated with additional
options. Not all of these options are required, and for everyday use, you
won’t need them. However, as you get deeper into JavaScript, you will find
reasons to use all of them.
If you don’t use then(), and try to console log a fetch request, you’ll
find the raw Promise instead, which will read “Promise {<pending>}” on
the console log.
248
Chapter 10 Fetching Data, APIs, and Promises
resolve(true)
// ...
reject(false)
249
Chapter 10 Fetching Data, APIs, and Promises
All promises have three methods attached to them, much like all objects
and strings have certain methods we can apply to them. These methods
are then, finally, and catch. You might find developers saying that
promises are thenable because you can always apply a then() method
to them. All three of these methods return a Promise, too, allowing you to
chain a never-ending sequence of promises.
In our first example, we used then to catch the promise and return its
data once the promise was resolved:
fetch("/some/api").then((res) => {
return res.json()
}).then((data) => {
let jsonData = data
})
The response of the fetch Promise is passed into the res variable,
which we can then parse as JSON using res.json(). The reason why
we can chain another “true” function is because then() always returns
a promise, meaning we can catch this new promise in another then()
function.
We can apply the same thought process to our previous example where
we used setTimeout() too:
250
Chapter 10 Fetching Data, APIs, and Promises
setTimeout(function() {
reject(false)
}, 1500)
})
myPromise.then((response) => {
console.log(response) // Console logs true
})
fetch("/some/api").then((res) => {
return res.json()
}).then((data) => {
let jsonData = data
}).catch((err) => {
console.log("We ran into an issue")
console.log(`Error: ${err}`)
})
251
Chapter 10 Fetching Data, APIs, and Promises
The main use case for finally is to perform any final steps after a
promise is done. For example, we could show something to the user on
their screen, once an API has fired:
fetch("/some/api").then((res) => {
return res.json()
}).then((data) => {
let jsonData = data
}).finally(() => {
// Show #final-dialogue to user
document.getElementById("final-dialogue").display = "block"
})
252
Chapter 10 Fetching Data, APIs, and Promises
getApiFunction()
Note await and then both have valuable uses. Deciding which
to use will depend on the structure of your code and how long APIs
will take to deliver. You don’t want to suspend all of your code due
to a slow API, but you may always need to wait for some APIs before
doing anything – like checking if a user is logged in via a login API.
All of these methods can be used with await, but the following
examples use then() and catch() instead. They all take arrays of promises
as input. The arrays can take as many promises as you like. They also all
return a Promise, letting you chain more promises onto them.
Promise.allSettled
Promise.allSettled waits for all promises in an array to finish before
producing a new Promise with the results from all of those promises:
Promise.allSettled([firstPromise, secondPromise,
thirdPromise]).then((data) => {
console.log(data)
})
254
Chapter 10 Fetching Data, APIs, and Promises
{
{ status: 'fulfilled', value: 'Hello World' }
{ status: 'fulfilled', value: 'Not Sure World' }
{ status: 'rejected', reason: 'Goodbye World' }
}
When using any other method, you will get only the final value of the
Promise, making Promise.allSettled() different in the way it returns
Promise outcomes.
Promise.all
Promise.all() only returns one final outcome, for example, in the
following code:
255
Chapter 10 Fetching Data, APIs, and Promises
If one promise had rejected, you would have to catch it using catch(),
and only the rejected promise would come back:
Promise.race
Promise.race() accepts an array of promises, too, and the fastest promise
is returned. Using our previous examples, “Goodbye World” is fastest so
using Promise.race() here returns the string, “Goodbye World”:
256
Chapter 10 Fetching Data, APIs, and Promises
Promise.any
Promise.any is a lot like Promise.race – it returns the value of the first
promise to fulfill. The major difference is that Promise.race will return the
first Promise which rejects or fulfills, while Promise.any will return only
the first promise that fulfills. For example, even though thirdPromise is
faster than secondPromise, Promise.any will return secondPromise as the
final value since thirdPromise rejects:
257
Chapter 10 Fetching Data, APIs, and Promises
Summary
In this chapter, we’ve covered a lot of key concepts relating to APIs and
asynchronous behavior in this chapter. At the beginning, we looked at
how HTTP requests work. We then went into some detail about how we
can create our own HTTP requests, and the key concepts behind RESTful
design. After that, we looked at how we can create our own servers in
Node.js and how these can be used to receive HTTP requests which
we send on the front end via the fetch() function. Since fetch() is
asynchronous, we’ve also gone into detail about how promises work,
which is the type of object returned for asynchronous responses in
JavaScript. Finally, we looked at how we can process promises using
keywords and other functions.
Asynchronous behavior is really important in JavaScript since
JavaScript’s architecture means it doesn’t automatically wait for the
response from an asynchronous action before continuing in its execution
of code. As such, having a strong grasp of these concepts is useful as
you start to build your own applications. As such, the different promise
methods we’ve covered in this chapter are vital to building modern web
applications.
258
CHAPTER 11
Introduction
to Web APIs
Writing JavaScript in the browser means that a lot of what we can do
is limited by what the browser allows us to do. While core JavaScript
functionality lets us create functions, math, and variables, what if, for
example, we want to use the operating system’s notification system from
our code? Or store something locally on the user’s computer? For each
of these, JavaScript in the browser implements various Web APIs to allow
programmers to access this functionality.
We’ve actually already encountered two of these APIs, that being the
HTML DOM document API for query selecting HTML DOM elements and
the fetch API for creating HTTP requests. In this chapter, we’ll go over Web
APIs in more detail and look at some very important ones.
Web APIs
In the previous chapter, we looked at how JavaScript can be used to create
and call APIs from servers. While these are Web APIs in the sense that
they are implemented on the Web, usually the term “Web API,” when
used in relation to JavaScript, refers to a special type of API. We have
briefly covered Web APIs throughout the book, and we’ve discussed how
they allow for a type of multithreading. How this works can be seen in
Figure 11-1.
Figure 11-1. Web APIs are processed separately from your main
JavaScript stack. They usually interact with the operating system or
run some more complicated code in the background (in languages
like C, C++, or Rust). Web APIs abstract the complexity into a few
function calls
The diagram in Figure 11-1 gives you a hint at how Web APIs differ
from APIs built on a server. Web APIs exist within your browser. They
usually provide an interface between JavaScript and the operating system.
Just like other APIs, Web APIs abstract complexity away from the
programmer. For example, the Web API we use for sending notifications
to the user via their operating system’s notification system may be
simple to call in JavaScript, but under the hood, this API will run some
more complicated code in other languages (like C and C++) to cause the
operating system to display a notification to the user.
Every Web API exists on the window object. That means that while we
could write window.fetch() to use the fetch API, it’s much more common
to omit window and simply run fetch().
260
Chapter 11 Introduction to Web APIs
The list of Web APIs is long, and new APIs are developed all the time.
As well as that, support often varies from browser to browser for many of
them. As such, we won’t cover them all but instead focus on the ones you
will find most useful for browser-based JavaScript development.
Web APIs usually arise from a specific need that is unfulfilled which
many developers frequently require. For example, the Web Storage API is a
Web API for storing data on a user’s local computer. Many Web APIs do not
have cross-browser support and are considered experimental, meaning
you need to be careful when using them. Although Web Storage is broadly
accepted these days, you may find developers checking if the browser
supports them by checking if they exist on the window object. All Web APIs
exist on the global window object, so you can check for this support as is
shown have done in the following example:
if(window.localStorage) {
// The browser supports local storage
}
261
Chapter 11 Introduction to Web APIs
URL API
Before the URL API, developers would have to write their own scripts to parse
a URL, and many times, these were poorly implemented. The URL API is a
simple and standard Web API that can parse URLs. It is implemented via the
URL constructor, which parses a URL and returns an object of its components:
262
Chapter 11 Introduction to Web APIs
localStorage.setItem("someKey", "someValue")
sessionStorage.setItem("temporaryKey", "temporaryValue")
263
Chapter 11 Introduction to Web APIs
264
Chapter 11 Introduction to Web APIs
document.getElementById("notification-button").
addEventListener(async (e) => {
await Notification.requestPermission()
})
The user will then be asked to review this permission request, after
which you can send them notifications. It’s important to know that
notifications can only be sent when the user has your web page open.
Notifications also have some additional options, letting us set the title,
body text, and icon for the notification:
someNotification.close()
265
Chapter 11 Introduction to Web APIs
History API
Before the History API, it was basically impossible to manipulate a user’s
history from JavaScript. The History API lets us do this from the history global
object. For example, we can force a user to navigate back and forward like so:
For the user, the page will not reload, but the URL will change. The
first parameter of pushState is data which is passed to the window’s
popstate event whenever pushState is fired. You can use this data to do
something else to the page inside the popstate event listener. The second
argument is never used and is usually left blank for historical reasons.
266
Chapter 11 Introduction to Web APIs
The last argument is the new page name of the URL. If you were on
https://google.com/ and fired this, the URL would change to
https://google.com/page.
While history.pushState() creates a new entry in the user’s history,
you can also use history.replaceState() to replace the URL for the
current entry in the user’s history, too. This works in exactly the same way;
only it won’t create a new entry in the user’s history, meaning pressing
back will just go back to the previous page the user was on:
Summary
The APIs we’ve covered previously all solve a specific need that JavaScript
developers either had to create custom solutions for or simply couldn’t do
before. There are many more useful Web APIs, and here are a few more
examples with more niche functionality, which have broad support in
most modern browsers:
267
Chapter 11 Introduction to Web APIs
When using Web APIs, it’s important to check their browser support.
Not all browsers support the same features in JavaScript, so some new Web
APIs may work in Chrome, but not in Safari, for example. You can explore
the full functionality of the preceding Web APIs and others in your own
time or when your project needs them.
Functions, while loops, variables, and math are all fundamental to
JavaScript, but it would be hard to do anything interactive on the Web
without the use of Web APIs. Web APIs give us additional functionality
on top of all the core tenets of JavaScript, like notifications, local storage,
and HTTP requests. While we haven’t covered every Web API here, the
cool thing about JavaScript is that new ones are always being created. In
the next couple chapters, we’ll look at more Web APIs that provide further
useful functionality to your JavaScript applications.
268
CHAPTER 12
let x = 5
console.log(x) // console logs 5
Console Tracing
If you have a long list of functions, you can trace the origin of them by
using console.trace. This outputs what is known as a trace to the console.
The resulting output of the following code can be seen in Figure 12-1.
function someFunction() {
console.trace()
}
someFunction()
270
Chapter 12 Errors and the Console
Console Assertion
Another way to create errors with the console is console.assert. While
related to console errors, console.assert is a little different in that
it accepts multiple arguments, and it throws an error only if the first
expression is false. The second argument is the error that will be shown
to the user. In the following example, we assert that 5 is equal to 4. Since
this is obviously false, an error with the error text, “the assertion is false,” is
shown to the user. This can be seen in Figure 12-2.
Figure 12-2. Console assertions let you show errors if certain criteria
are found to be false
Console Timing
Typical JavaScript code is not exactly known for its efficiency. If you write bad
code in JavaScript, it has the propensity to take a really long time to process. For
example, it’s possible to write code that pauses execution via promises, waits
for APIs to load, or has inefficient calculations that result in long wait times.
271
Chapter 12 Errors and the Console
All of these impact page load time and therefore user experience.
Search engines like Google use page load time to figure out where to
place your website in search results. Therefore, optimizing your JavaScript
is really important, and the best way to find code that is performing
code is by measuring how long it takes to run. To help diagnose
these problems, there are three console methods we can use. These
are listed below:
• console.time
• console.timeLog
• console.timeEnd
console.time("my timer")
setTimeout(function() {
console.timeLog("my timer")
}, 1000)
setTimeout(function() {
console.timeLog("my timer")
console.timeEnd("my timer")
}, 1500)
272
Chapter 12 Errors and the Console
my timer: 1002.744140625 ms
my timer: 1506.8349609375 ms
my timer: 1507.031005859375 ms
As you can see, this is roughly correct, give or take a few milliseconds
for processing the console method itself. You can call console.timeLog as
many times as you want, and console.timeEnd will also result in one line
of time recording in your console.
Console Counting
Another method that works the same as console.time is console.count. It
accepts a unique value, and every time it is called, it will add 1 to the count
for that value. For example:
console.count("my count")
console.count("my count")
console.count("my count")
my count: 1
my count: 2
my count: 3
If you want to reset the counter to the beginning again, you can then
use console.countReset("my count").
273
Chapter 12 Errors and the Console
console.log(console)
We’ve covered many of the most useful console methods, and you
can mess around with these in your own time. Let’s look at the remaining
methods and how they work in more detail.
Console Grouping
You can group console logs with console.group and console.
groupCollapsed.
Both indent console logs, creating multiple levels of console. The main
difference between both is that when using console.groupCollapsed, the
groups are collapsed, and the user must expand them to read the console.
In the following example, we create two levels of console log. The output of
this can be seen in Figure 12-3.
274
Chapter 12 Errors and the Console
Console Tables
Finally, console.table allows you to create tables of objects or arrays. For
example, given the following input, we can produce a table as is shown in
Table 12-1.
const favoriteFruits = [
["Person", "Favorite Fruit"],
["John", "Apple"],
275
Chapter 12 Errors and the Console
["Mary", "Raspberry"]
]
console.table(favoriteFruits)
John Apple
Mary Raspberry
276
Chapter 12 Errors and the Console
277
Chapter 12 Errors and the Console
try…catch
When we generate errors in our code, usually they terminate execution.
That means JavaScript stops trying to process anything else. We obviously
don’t want that to happen, since it will break the user experience!
Sometimes though, errors can be unavoidable. For example, passing
non-JSON values to JSON.parse() will usually result in an error. This can
happen quite easily, especially if you are working with badly written APIs.
To prevent these kinds of errors from breaking our code, we can use
try...catch:
try {
let JSONData = JSON.parse(inputData)
}
catch (e) {
console.log("Could not parse JSON!")
let JSONData = {}
}
If the code passes with no errors, then great – we have parsed some
JSON. If it does not, then the error is “caught” in the catch statement.
The e in catch refers to the error text itself with a trace of where the error
came from.
278
Chapter 12 Errors and the Console
try {
throw "Some Error"
}
catch (e) {
console.log("An error was generated")
}
finally
We can supplement try...catch with a finally statement, which will
immediately fire before a control statement from a try or catch block or
before execution completes where a control statement does not exist.
A finally statement does not catch the error. That means if you omit
the catch, an error will still be thrown. In the following example, finally
will run before the error appears since finally runs immediately before any
control statements (think return, throw, break or continue) complete.
That means JSONData is defined as {} in the following example even
though an error is still thrown (since “ok” is not valid JSON):
try {
let JSONData = JSON.parse("ok")
}
finally {
let JSONData = {}
}
279
Chapter 12 Errors and the Console
If we had caught this error, then JSONData would have been set to {},
and the error would not have been shown at all. This allows us to separate
out the catching of the error from defining alternatives:
try {
let JSONData = JSON.parse("ok")
}
catch(e) {
console.log("Your JSON is invalid")
}
finally {
let JSONData = {}
console.log(JSONData)
}
Since finally will run control statements before try and catch complete,
it can be used to mask the value of try and catch control statements. For
example, in the following example we try to return some valid JSON. This
is returned, but finally always fires before the last control statement in
a try or catch. As such, the return value is set to {} since that is what
finally returns. A diagram of the try...catch...finally flow can be found in
Figure 12-4.
function someFunction() {
try {
return { "hello" : "world" }
}
finally {
return {}
}
}
280
Chapter 12 Errors and the Console
Figure 12-4. Finally will always fire before try and catch control
statements. However, the rest of the try and catch code before the
control statement will be executed before finally
Generating Errors
We have already seen how throw can be used to generate errors. Errors
generated with throw stop code execution, unless caught in a try...catch
statement.
We can get more control over our error statements by using the new
Error() constructor instead. However, by itself, new Error() does not
actually generate a new error as the code suggests! Instead, it creates the
text that would appear in the error. In the following example, the error
generated will not stop execution but generate a text string with the line on
which new Error() was generated from. You can see what that looks like
in Figure 12-5.
281
Chapter 12 Errors and the Console
Figure 12-5. Using the Error constructor does not create a new error
but rather creates the text that would appear for that error
try {
JSON.parse("OK")
} catch(e) {
if(e instanceof SyntaxError) {
console.log("This is a syntax error");
} else if(e instanceof EvalError) {
console.log("This was an error with the eval()
function");
}
}
282
Chapter 12 Errors and the Console
Summary
In this chapter, we’ve covered all the key principles around JavaScript
consoles. We’ve shown you that there are many useful methods found
on the console object, which can be used to bring more meaning to your
console logs.
We’ve also discussed errors. We’ve covered all the types of errors
that JavaScript can throw at you and how to catch errors through try...
catch...finally statements. Finally, we’ve shown how you can generate
your own errors using throw statements.
Error management and console logging are really important in
ensuring your users have a good experience. Ideally, users should not be
exposed to raw errors, and developers should have access to good console
logs. By becoming familiar with everything we’ve covered here, you can
begin to add these best practice principles into your code.
283
CHAPTER 13
The Canvas
So far, we have only dealt with code that is used either directly in
JavaScript or on the HTML DOM. Operations to modify the DOM are
easy to do in JavaScript, but they are relatively slow when compared to
other JavaScript code. You’ll begin to notice these inefficiencies if you
have thousands of DOM elements on your page and apply interactions to
them using addEventListener. This makes creating DOM-based games
or large interactive applications like whiteboards nearly impossible from
a performance perspective. At one point, I learned this the hard way
when trying to recreate the original Mario game by using a one HTML
DOM element to a one-pixel scale. Eventually, such complex applications
become totally unresponsive.
Some JavaScript frameworks try to improve on DOM inefficiencies by
converting the entire DOM into an object, called the Virtual DOM. This
is useful for websites wanting to load faster but still does not address
the former problem, where just having many DOM elements becomes
problematic in large applications.
To get around this problem, and others, we have an HTML element
called <canvas> and an associated JavaScript Canvas API, which lets
us draw interactive content onto a single HTML element. Canvas is just
normal JavaScript code, with a few additional functions and methods
that allow us to draw. It is much more efficient than large scale DOM–
based applications but is not really recommended as a way to build entire
websites in. Instead, canvas should be used for small pieces of interactive
content, interactive games, or parts of interactive applications. In this
chapter, we’ll cover how the Canvas API works in JavaScript.
© Jonathon Simpson 2023 285
J. Simpson, How JavaScript Works, https://doi.org/10.1007/978-1-4842-9738-4_13
Chapter 13 The Canvas
Introduction
To get started with HTML canvas, we first need to create a <canvas>
element. This empty tag becomes the container for the graphic we
will produce via JavaScript. In HTML, we can create a canvas by writing the
following code:
You may see canvas with predefined width and height, which is useful
if the graphic we’re producing has to have a certain size. You could set your
canvas to be of width and height 100%, too.
Once set up, we can reference our canvas in JavaScript using any
JavaScript selector method:
console.log(context)
By using these methods, we can draw directly onto the canvas HTML
element.
The preceding object created is a CanvasRenderingContext2D object
since the context we used is 2D. That means the expectation is we will only
create 2D drawings on our canvas. Additional contexts can be applied. The
full list is shown in the following:
286
Chapter 13 The Canvas
• canvas.getContext("2d") – Creates a
CanvasRenderingContext2D object for 2D drawing.
• canvas.getContext("webgl") – Creates a
WebGLRenderingContext object. This is only available
if your browser supports webGL but gives additional
methods for working in a 3D space.
• canvas.getContext("webgpu") – Creates a
GPUCanvasContext object. This is only available if your
browser supports webGPU – the successor to webGL.
• canvas.getContext("bitmaprenderer") – Creates an
ImageBitmapRenderingContext object. This is used
for drawing images onto the canvas. ImageBitmaps are
more performant than other image data since they are
processed directly on the GPU.
All of these settings create objects with useful methods in each context.
In this chapter, we will focus on the 2d context as it is the most widely used.
From here on out, most of the work on our canvas element stems from the
context variable.
287
Chapter 13 The Canvas
Drawing Rectangles
Three main methods exist for drawing rectangles:
For each of these the arguments x, y, width, and height refer to a pixel
coordinate to draw the rectangle. Therefore the width and height of your
canvas become quite important. The width and height you set on your
canvas element is the drawing location.
For example, if your canvas was 400px by 600px in size, drawing
something at point 500px, 700px would be not visible, as it would no
longer be on the canvas – however, drawing something at 200px, 200px
would be 200px in on both the x and y coordinates.
Note If you change the size of your canvas with CSS, it will not
affect the drawing area; that can only be affected by the width and
height properties. That means that if the canvas HTML element is
200px by 200px, and you set width and height to 100px by 100px,
the resolution of the canvas will double!
288
Chapter 13 The Canvas
By themselves, the rectangle functions are not of much use. You need
to combine them with other methods for them to become visible. For
example, if we want to create a rectangle, context.rect() will create
the “information” about where the rectangle is, but context.fill() and
context.stroke() are required to give it a color and outline, respectively:
By default, the stroke and fill color are both black. We can further
enhance our drawings by setting custom stroke widths, fill colors,
and stroke colors by using lineWidth, fillStyle, and stokeStyle,
respectively:
It’s worth noting that canvas in HTML is sequentially written. The fill
style will apply to any drawn elements until another is drawn. So if we
wanted to draw two rectangles beside each other in different colors, we’d
have to update the values of fillStyle and strokeStyle. We also have to
start each new shape with the beginPath method. That way, canvas will
289
Chapter 13 The Canvas
know that the fill and stroke styles apply only to specific rectangles. You
can see an example of that code in the following and an illustration of what
it will look like in Figure 13-1.
context.beginPath()
context.rect(10, 10, 100, 150)
context.fillStyle = "#8effe0"
context.strokeStyle = "#42947e"
context.lineWidth = 5
context.fill()
context.stroke()
context.closePath()
context.beginPath()
context.rect(120, 10, 100, 150)
context.fillStyle = "#42947e"
context.strokeStyle = "#8effe0"
context.lineWidth = 5
context.fill()
context.stroke()
290
Chapter 13 The Canvas
Drawing Circles
Two methods exist on 2d canvas contexts to draw circles:
The arc() method has the following arguments, with the last being
optional:
The arguments here are similar. Here is the full list along with
definitions:
291
Chapter 13 The Canvas
Since both use radians for degrees, we have to use the Math.PI
constants to refer to angles for each. In the following example, we create a
full circle using context.arc(), since 2π is equal to 360 degrees:
292
Chapter 13 The Canvas
When creating ellipses, two radii are used: radiusX and radiusY allow
us to squish or morph a circle along two axes. Let’s add an ellipse to our
group, too. You can see all three of our circles in Figure 13-2.
context.beginPath()
context.ellipse(350, 100, 45, 25, 0, Math.PI * 2, false)
context.fillStyle = "#42947e"
context.fill()
Figure 13-2. Our final group of circles – one created using Math.PI
* 2 radians, the second Math.PI radians, and the final one using two
radii via the ellipse() method
Drawing Triangles
Since there are no triangle methods in canvas, we have to make our own.
To do that, we need to learn about the moveTo and lineTo methods. The
first moves the starting point of a line to a specific position, and the second
293
Chapter 13 The Canvas
context.moveTo(20, 0)
context.lineTo(40, 30)
context.lineTo(0, 30)
context.lineTo(20, 0)
context.fillStyle = "#42947e"
context.fill()
Since moveTo does not draw lines, we can draw multiple triangles
(or indeed, as many different types of shapes as we want using moveTo
and lineTo. In the following, we draw two identical triangles using this
method. You can also see this code in action in Figure 13-3.
context.moveTo(20, 0)
context.lineTo(40, 30)
context.lineTo(0, 30)
context.lineTo(20, 0)
context.moveTo(80, 0)
context.lineTo(100, 30)
context.lineTo(60, 30)
context.lineTo(80, 0)
context.fillStyle = "#b668ff"
context.fill()
Figure 13-3. Two identical triangles drawn using the moveTo and
lineTo methods
294
Chapter 13 The Canvas
Drawing Images
While drawing shapes is useful when working with the canvas, you may
also find yourself needing to add images. Images work a little differently
from what we’ve seen so far. To create a new image, we have to use the
image constructor to add them to our canvas.
Since images are loaded over HTTP, we also need to wait for them to
load before we can add them to our canvas. Here is an example of how we
do that:
// When it loads
newImage.onload = () => {
// Draw the image onto the context
context.drawImage(newImage, 0, 0, 250, 208);
}
295
Chapter 13 The Canvas
The definitions for each argument in this version are shown in the
following list. You can also see a demonstration of how these arguments
alter the image in Figure 13-4.
• cx – This is how far from the top left we want to crop the
image by. So if it is 50, the image will be cropped 50px
from the left-hand side.
296
Chapter 13 The Canvas
Drawing Text
Now that we’ve covered images and shapes, let’s take a look at how we can
add text to a canvas. As with previous examples, various functions and
properties also exist to add text to a canvas:
The preceding example will create text with a 4px red stroke, at
position 100x200px. We can also use fillText(), if we want to fill the text
with a specific color instead. Using either does not prevent us from using
fillStyle with fill() or strokeStyle with stroke(), so you can decide
which method to use based on what is more useful.
It’s worth noting that by default, the 100x200px is taken from the
bottom left of the text, at the text’s baseline, rather than the top left. To
change where the y coordinate is drawn from, we can change the text’s
baseline. For example, the following code will draw the y coordinate from
the middle of the text instead:
context.textBaseline = "middle"
context.textAlign = "center"
298
Chapter 13 The Canvas
• The user can then draw more lines if they want to.
For this example, we’ll be using the following HTML canvas element:
299
Chapter 13 The Canvas
therefore dragging is occurring. If they let go, then clicked is false, and
dragging has stopped. While clicked is true, we can track the user’s
cursor position and draw a rectangle in the right place:
let clicked
canvas.addEventListener("pointerdown", (e) => {
clicked = true
})
Since the down and up events only control when drawing starts and
stops, let’s focus in on pointermove, where most of the logic for drawing
will sit. There are two things to consider here:
Therefore, we need to get the canvas’ top left position and subtract this
from the cursor position in order to calculate where the user is clicking
inside the canvas. An image demonstrating how this works can be found in
Figure 13-5.
300
Chapter 13 The Canvas
301
Chapter 13 The Canvas
All of these properties include padding and border width, but not
margin. So x refers to where the border starts, and not the margin, for
example. We can access these on our canvas element by selecting the canvas
element, and then applying the appropriate getBoundingClientRect()
method. For example, we can get the x coordinate like so:
The final step in our canvas interactivity use case is to draw a line as the
user moves their “pressed down” cursor. To simplify this, I’ve decided to just
draw a 5x5px rectangle every time the user drags their cursor. Our final code
lets us draw on the canvas element, as expected in the original requirements.
let clicked
302
Chapter 13 The Canvas
context.rect(cursorLeft, cursorTop, 5, 5)
context.fill()
}
})
Animations
The final piece of functionality that the Canvas API can help us with is
smooth animations. To perform animations in general, we usually use
the top-level requestAnimationFrame() method. This is actually a pretty
cool function, which calls any function passed to it 60 times per second,
producing an effective frame rate of 60fps.
The function passed to requestAnimationFrame gains an argument,
usually referred to as timestamp, which is the current timestamp in
milliseconds for the request. Since requestAnimationFrame calls 60 times
per second, you won’t get a result for every millisecond!
In the following example, we use requestAnimationFrame to create a
recursive function. A recursive function is simply one that calls itself again
and again, which means the function can be called continuously on a
60fps cycle. For this animation, we’ll be using this HTML canvas tag:
303
Chapter 13 The Canvas
Since this would just last forever, we limit this recursion until the
timeStamp is more than 50000 milliseconds – or 50 seconds. That will
result in an animation which is 50 seconds long. This way, we don’t end up
with a function that runs until infinity.
The following animation creates a rectangle every 10 pixels, giving
the illusion of a box that is filling up with 10x10px rectangles. While
this is quite a simple animation, you can imagine how this could be
changed to make more complicated computations. You can see how this
animation looks part way through in Figure 13-6.
let y = 0
let x = -500
function drawCanvas(timeStamp) {
x += 10
let xCoordinate = x + 490
if(x > 0) {
x = -500
y += 10
}
context.rect(xCoordinate, y, 10, 10)
context.fill()
// If timestamp is less than 50s, then run the animation
// keyframe
if(timeStamp < 50000) {
requestAnimationFrame(drawCanvas)
}
}
requestAnimationFrame(drawCanvas)
304
Chapter 13 The Canvas
Summary
In this chapter, we’ve covered everything you need to get started with
HTML canvas. You should know enough from this chapter to play around
with it yourself. We’ve discussed how you can draw shapes, add text or
images, and then subsequently add interactivity to your canvas via event
listeners. We’ve also covered animations, which can be used to create
interactive experiences for users in the browser.
The Canvas API is an efficient way to draw content for users or add
interactivity to certain views. It also supports 3D contexts too, which can
be utilized to create games in the browser. We won’t cover here how 3D
contexts work, but a popular framework called Three.JS can help you get
started with that.
305
CHAPTER 14
Introduction
Web workers are an advanced concept, and they are not needed for
everyday JavaScript. If you are creating a website, you probably don’t
need web workers. In fact, you won’t need web workers to do anything in
JavaScript – but you’ll probably want them for some things.
An example of where web workers become useful is when you have
written something that requires heavy computation. For example, imagine
you have an advanced image processor. If the image processor takes
Figure 14-1. Messages can be sent to web workers, causing the web
worker to start processing. Then, an output will be produced, which
can be captured on the main stack by events, for processing by the
main thread
308
Chapter 14 Web Workers and Multithreading
Since web workers still result in code coming back to the stack, you
may need to think about how you outsource computation. For example,
an API running via fetch still has its own thread on the server and does
computation asynchronously without affecting your front-end JavaScript
code. Therefore, an API may be all you need to fulfill your needs. Web
workers are most useful when you want to create multithreaded code
locally, without relying on API servers.
Web worker files are in a special format, and in the browser, they
consist of a single onmessage event. Inside that, there is usually a
postMessage function, which contains the data we want to send back to
the main thread. For example, a simple web worker file, like thread.js,
might look like this:
onmessage = function(e) {
postMessage(`Worker Data: ${e.data}`)
}
309
Chapter 14 Web Workers and Multithreading
onmessage = function(e) {
postMessage(`Worker Data: ${e.data}`)
}
In our main JavaScript file, we can send data to our worker by using
postMessage on the worker:
document.getElementById('button').addEventListener('click',
function(e) {
myWebWorker.postMessage({ "message" : "outcome" });
});
onmessage = function(e) {
if(e.data.startScript === true) {
// .. Do something
310
Chapter 14 Web Workers and Multithreading
postMessage({"processing" : "done"})
}
}
When the worker sends data back to the main thread, we can capture
the data using the onmessage method attached to the worker constructor:
Note Web workers can use fetch() just like regular JavaScript
code. That means you can outsource expensive APIs to web workers
where necessary.
Web workers also have an onerror method, for capturing any errors
that occur inside your web worker:
311
Chapter 14 Web Workers and Multithreading
Since web workers have some restrictions on the types of methods that
can be called, it can be useful to console log the globalThis property inside
your web worker thread. You can also find this information online, via
websites like developer.mozilla.org:
console.log(globalThis)
You can see the different methods available to you in web workers by
console logging globalThis. The outcome can be seen in Figure 14-2.
312
Chapter 14 Web Workers and Multithreading
Conclusion
Web workers are a useful way to create multiple threads in your JavaScript
code. While Web APIs and servers also give you ways to create multiple
threads, web workers are a dedicated feature that allows you to create
multiple threads independently.
JavaScript is quite performant because of the event loop and stacks.
Therefore, web workers are only really useful for the most intensive
computation functions. Although you shouldn’t use them everywhere,
they can come in very useful in certain use cases and are thus another tool
in your arsenal for creating good JavaScript code.
313
Index
A Arrow functions, 106, 107
Assignment operators, 26, 27
addEventListener, 173, 182,
template literals, 29
184, 285
variable concatenation, 27, 28
Animations, 303–305
Asynchronicity, 248–257
Anonymous functions, 103–104
Application programming
interfaces (API), 90 B
advantages, 224
Back-end JavaScript, 2, 9–12
canvas (see Canvas)
Bitmap, 288
description, 224
Block scoping
HTTP, 226–240
logical statements, 36–38
principles, 225
variables, 21, 22
REST, 225
Bubble phase, 180–185
servers/web pages, 226
Array.prototype, 79, 80, 167, 168
Arrays, 43, 44, 94, 102, 110, 138, C
200, 253, 275, 276 Canvas
duplicates, 205 additional contexts, 286, 287
forEach methods, 78 animations, 303–305
last element, 45, 46 API, 285
length, 44, 45 arguments, 291–292
manipulation methods DOM, 285
pop and shift, 47 drawing application, 299–303
push and unshift, 47 element, 288
splice, 48, 49 HTML, 286
NodeList, 167, 169 interactivity, 299–303
and objects, 76 in JavaScript, 286
promises, 256 Capture phase, 183–185
316
INDEX
317
INDEX
318
INDEX
319
INDEX
320
INDEX
321
INDEX
V W, X, Y, Z
Variables, 65–67, 70, 88, WeakMaps, 217
94, 179, 216, 241, Web APIs, 91, 105, 159, 226
250, 277, 279 History API, 266, 267
arrayLength, 45 JavaScript, 259, 260, 268
block scoping, 21, 22 notifications, 260, 261
concatenation, 27, 28 URL API, 262
conditional Web Notification API, 265
operator, 38 Web Storage API, 261, 262
context, 287 window object, 260
CSS, 200, 201 WebGL, 288
definition, 72 WebGPU, 288
destructuring Web Notification API, 261, 265
objects, 53–55 Web Storage API, 261
drag, 192 back-end storage, 262
e variable, 194, 195 IndexedDB API, 264
function scope, 101 localStorage, 262–264
global, 110 sessionStorage, 262–264
logical AND operator, 151 Web workers, 309
and logical computation, 307
statements, 75 JavaScript, 307
name, 103 messages, 308
non-object type, 90 restrictions, 311–313
setting sending messages, 309–311
with const, 23–25 Window
with let, 20–22 HTML document, 159, 160
with var, 22, 23 HTML elements, 163, 164
type string, 134 object, 160–162
VeganHotSauce, 126 window.fetch(), 260
without values, 25, 26 Wrappers
VeganHotSauce, 125–129 to create types, 136, 137
Visual Studio Code (VS object, 135, 136
code), 6–8, 18 primitives, 133–136
322