How JavaScript Works Master The Basics of JavaScript and Modern Web App Development-1
How JavaScript Works Master The Basics of JavaScript and Modern Web App Development-1
This work is subject to copyright. All rights are solely and exclusively
licensed by the Publisher, whether the whole or part of the material is
concerned, specifically the rights of translation, reprinting, reuse of
illustrations, recitation, broadcasting, reproduction on microfilms or in
any other physical way, and transmission or information storage and
retrieval, electronic adaptation, computer software, or by similar or
dissimilar methodology now known or hereafter developed.
The publisher, the authors, and the editors are safe to assume that the
advice and information in this book are believed to be true and accurate
at the date of publication. Neither the publisher nor the authors or the
editors give a warranty, expressed or implied, with respect to the
material contained herein or for any errors or omissions that may have
been made. The publisher remains neutral with regard to jurisdictional
claims in published maps and institutional affiliations.
1. Introduction to JavaScript
Jonathon Simpson1
(1) Belfast, Antrim, UK
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.
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.
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.
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>
<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>
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:
Then you can execute this file by using the node command in
terminal:
node index.js
Figure 1-3 Running a JavaScript script from the command line is as simple as using the “node”
command followed by the directory link to the file you want to run
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.
Node.js applications like this are frequently used to create APIs,
which we will cover in much more detail later in the book.
Creating Node.js JavaScript Projects
In the above example, we executed a single file using Node.js It is more
common, though, to create a Node.js project when you start something
new in Node.js. This is also done via terminal or the cmd. The first step
is to make a new folder and navigate to it using the cd command.
In this example, I made a folder called “node-project” on my desktop
and navigated to it using the following command in Terminal:
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.
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 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.
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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_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!")
Figure 2-1 Right-clicking in the web browser and selecting “Inspect” in Google Chrome (or
other browsers) allows you to access the console log. If you do this with the index.html file we
defined before, you’ll see “Hello World!” written here. The console is a powerful tool used for
debugging. When we use the console.log method, it shows up here!
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.
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.
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
All JavaScript variables are case-sensitive, so myVariable is
different from MYVARIABLE. In JavaScript nomenclature, we
usually say that we “declare” variables.
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)
Figure 2-2 Variables defined with let cannot be defined multiple times. If you try to, it will
produce an error like the preceding one
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)
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
Setting Variables with var
Most variables are set with let, but you may also see the keyword var
being used to set variables sometimes. Using var is the original way to
set variables in JavaScript. It is valuable to know this exists, but it is
generally not recommended that you use it over let.
Setting a variable with var looks a lot like what we did with let.
For example, here is a variable called myVariable, with a value of 5:
var myVariable = 5
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)
const myConst = 5
console.log(myConst)
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.
Arrays can contain a lot of data, and we can push new data to an
array using a special method called push:
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:
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)
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
Just be careful, since if you try to use a + with numbers, it will add
them up instead!
let myVariable = 5
let myOtherVariable = 5
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 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:
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! */
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.
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.")
}
Figure 2-4 Since myVariable is 5, the statement myVariable === 5 is always true. So the
block of code within the first if statement is executed, and the else statement never fires
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:
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")
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":
console.log("Apples and Strawberries can
be red.")
break
case "Bananas":
console.log("Bananas are yellow.")
}
// Let's set x to 5
let x = 5
switch(x) {
case 4: {
console.log("hello")
break
}
default: {
console.log("goodbye")
break
}
}
// Let's set x to 5
let x = 5
switch(x) {
case "5": {
console.log("hello")
break
}
case 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
}
// 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 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:
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!
Figure 2-6 Other operators can be used to test logical statements. In the preceding example,
we use the more than operator to check if myVariable meets certain conditions
We can check for “regular” equality too, which ignores type, using
the double equals (==) operator:
let myVariable = 5
if(myVariable == '5') // True!
Operators Definition
&& AND – something AND something else is true.
|| OR – something OR something else is true.
! NOT – something is NOT true.
let x = 6
if(x > 5 && x <= 10) {
console.log('Hello World!')
}
let x = 6
if(x > 5 || x > 3) {
console.log('Hello World!')
}
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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_3
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:
console.log(arrayLength) // shows 4
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)
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 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:
Figure 3-1 Using the square bracket notation, we can access the value of any key on an object
in JavaScript
let myObject = {
"key": "value",
"someKey": 5,
"anotherKey" : true
}
console.log(myObject.key) // shows 'value'
let myObject = {
"key": "value",
"someKey": 5,
"anotherKey" : true
}
let keyName = "key"
console.log(myObject[keyName]) // shows "value"
console.log(myObject.keyName) // shows undefined
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
}
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:
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
}
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
}
console.log(myObject['key']['newKey']) // shows
'value'
console.log(myObject.key.newKey) // shows 'value'
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:
Writable – True if the property’s value can be changed, false if it is
read-only.
Enumerable – True if the property will be shown in loops, false if it
won’t be.
Configurable – True if the property’s value can be deleted or
modified, false if it cannot.
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 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.
Figure 3-3 In this image, Object.freeze means myObject can no longer be changed. If we
had used Object.seal instead, then changing via myObject["name"] would have worked,
while trying to add the age property would still have failed
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.
Figure 3-5 All objects in JavaScript have a prototype property
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:
Figure 3-6 Console logging any object in JavaScript can be quite revealing – and doing so on
array shows us all possible methods we can use on every array. Remember this tip if you get
confused when writing JavaScript!
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
},
"anotherKey" : true
}
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:
1.
Checks the object, myArray, for the property at.
2.
If it doesn’t exist, checks the object myArray’s prototype for the
property.
3.
If it still doesn’t exist, checks the object myArray’s prototype’s
prototype for the property.
4. Keeps doing this until no more prototypes exist, at which point it
will return undefined.
That’s why myArray.at(-1) works, but it doesn’t exactly
explain why myArray.prototype.at(-1) work. The reason for
this is that myArray.prototype is undefined. If you try console
logging a new array, you’ll find a [[Prototype]] property, but no
prototype property. Since arrays inherit these methods in a special
prototype section and not on the object itself, we can’t access methods
via myArray.prototype. This can be seen in Figure 3-7.
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]]
Figure 3-8 The prototype of an object has a property called __proto__, as shown earlier
In summary:
The prototype property found on array and object is a set of
all properties inherited onto arrays and objects [[Prototype]]s
when we make new instances of them (with array or object literal
notation, or new Array/Object()).
The [[Prototype]] is a hidden property, which exists on all
objects and can be seen by using console.log(). When you make
a new instance of an Array or Object, the [[Prototype]] of
that new instance points toward the prototype property of
Array/Object.
The __proto__ property is a deprecated way of accessing an
objects [[Prototype]]. It is strongly discouraged and is not
implemented the same in every browser or JavaScript engine.
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:
Figure 3-9 In the preceding image, the underlying “DATA” is the “value.” Then myArr and
newArr are both references to that data
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:
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);
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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_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
}
Since we don’t want a while loop to run indefinitely, we often adjust
the variable or condition each time the while look runs. In the previous
example, every time while runs, we also use the ++ operator to add 1 to
x. ++x is shorthand, and it’s the equivalent to writing x += 1. The
opposite of ++ is -- which subtracts 1 from a variable. If you didn’t do
this, you’d create an infinite loop, which would ultimately cause your
code to break! A summary of how the while loop above works is shown
in Figure 4-1.
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.
Figure 4-2 By using the while loop, we can do an action multiple times, without having to
write out what we want to do many times over. It’s important to modify a participant in your
while condition; otherwise, you may get an infinite loop
let x = 1
do {
console.log("hello world")
} while(x < 1)
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.
Table 4-1 A breakdown of the for statement
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.
To solve this, we can use labels to define which loop is supposed to
break and continue. Here’s an example of a labeled loop, where I
use the labels xLoop and yLoop to refer to the outer and inner loop,
respectively:
Figure 4-3 In this example, when x is equal to 2, xLoop is broken. That means the whole loop
is only run twice. If you broke yLoop instead when x was 2, then xLoop would continue to run,
while yLoop would have been stopped
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 to do something with all items in an array or object?
We could write them out on separate lines, as is shown in the following
example, but this quickly becomes unmanageable as the arrays and
objects become bigger:
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)
}
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)
}
let x = []
x[5] = "some value"
for(let item of x) {
console.log(item)
// Will show:
// undefined, undefined, undefined, undefined,
undefined
// "some value"
}
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.
for(let item of "hello") {
console.log(item)
}
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.
Figure 4-6 You can see the iterator protocol by console logging an iterable’s prototype, like
console.log(Array.prototype)
If you keep running next(), you’ll keep getting the next item, as is
shown in the following example:
let myObject = {
firstName: "John",
lastName: "Doe",
age: 140
}
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
}
Figure 4-8 Methods like Object.keys() turn objects into iterable arrays, which we can
then iterate through
let myObject = {
firstName: "John",
lastName: "Doe",
age: 140
}
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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_5
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?
The stack is a scratch space for the current JavaScript thread.
JavaScript is typically single-threaded, so there is usually one stack
per application. The stack is also limited in size, which is why
numbers in JavaScript can only be so big.
The heap is a dynamic memory store. Accessing data from the heap
is more complicated, but the heap is not limited in size. As such, the
heap will grow if needed.
The heap is used by JavaScript to store objects and functions. For
simple variables composed of numbers, strings, and other primitive
types, the stack is typically used instead. The stack also stores
information on functions which will be called.
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.
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
Sometimes, when running complex code or loops, you can see the
stack in action. If you exceed the stack limit, you’ll get the error,
RangeError: Maximum call stack size exceeded.
Different browsers and implementations of JavaScript like Node.js have
different stack sizes, but you have to be running a lot of code
simultaneously to ever get this error. You may also run into this error if
you accidentally run an infinite loop.
If you try to reassign a variable of primitive type, it gets added to the
stack as well, even if the variables are supposedly pointing to the same
value. Consider the following code, for example:
let myNumber = 5
let newNumber = myNumber
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.
let myNumber = 5
setTimeout(function() {
console.log("Hello World")
}, 1000)
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!
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
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
Figure 5-5 While non-objects are made on the stack as normal, a new object creates a new
reference. Even though both cloneObject and newObject have the same underlying
“value,” their references still differ
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
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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_6
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.
Inputs in functions are called arguments, and we return outputs
using the return keyword. Then, when we run a function, it returns
that value. If your function doesn’t return anything, that is also
acceptable, but better to avoid if possible.
When you don’t return something from a function in JavaScript, the
function will always return “undefined” by default – so the return
keyword is technically optional in a function.
So what does a function look like? In the following example, we
create a simple function that returns “Hello World” upon being run:
function myFunction() {
return "Hello World"
}
console.log(myFunction())
Figure 6-1 A function can be run in any context, even within another function. Since the return
value of the preceding function is “Hello World,” running myFunction will return that string,
which can then be console logged
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.
console.log(words("Hello", "World"))
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")
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.
Functions with Arrow Notation
The arrow notation is another way of defining functions. This notation
comes with some different functionality compared to the other function
definitions we’ve covered. The arrow notation is called as such because
it uses => to indicate where the body of the function begins.
Here is how our previous function looks with the arrow notation.
When you try to call it, it works the same as our other function
expressions:
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.
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 { }
"use strict"
let contextualFunction = function() {
let words = () => {
console.log(this) // console logs
undefined
}
words()
}
contextualFunction() // console logs undefined
"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
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)
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.
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
User.prototype.giveName = function() {
return `My name is ${this.fullName}!`
}
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)
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}
function* someGenerator(x) {
let index = 0
while(true) {
yield x * 10 * index
++index
}
}
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.
Classes and Constructor Functions
Classes can have a constructor function, which is the function that
runs any time they are called, but they don’t need to have one. In
classes, constructor functions are defined using the constructor
keyword, and you can only have one constructor function per class.
They behave like normal functions, and any arguments passed to the
class will go into the constructor function, as is shown in the following
example:
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.
There are a few things worth noting here:
1.
At the top level of the class body, variables can be defined within a
class without variable keywords. That’s because they act like the
properties of an object. Variables at the top level of the class join
the class’s context. In this example, I assign units and
maxHotness to the context of the class, so can be accessed via
this in methods.
2.
Methods are written like method() instead of function
method().
3.
Classes don’t have arguments, but you can call a class with
arguments. The arguments on the constructor are where the
arguments used on the class are passed to.
4.
Classes ultimately create objects, just like functions. If you try
console logging new HotSauce('Chilli Wave', 4600),
you’ll get an object back, as shown in Figure 6-5.
5. Finally, arguments passed to a class are not available by default to
the entire class. As such, it’s pretty common to see code that takes
arguments from the constructor function and assigns them to
this. That’s what we do in the following for the name and hotness
of the hot sauce defined:
constructor(name, hotness) {
// We can assign arguments from new
i t f
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!`
}
}
}
Figure 6-5 Classes create objects. If we log the instance we created of our HotSauce class,
newSauce, we’ll get an object containing all the properties we defined, along with a prototype
containing our methods
Class Method Types
By default, all class methods are public. That means they can be
changed outside of the class. They will also show up in the console log
and can even be deleted. For example, we could totally rewrite our
getName method if we wanted to, outside of the class:
static classDetails() {
return `${this.className} by
${this.author}`
}
}
If we try to initialize this class and call classDetails(), we’ll
receive a type error since classDetails cannot be called on a new
instance of Utility.
Utility.classDetails = function() {
return "Some return text"
}
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:
checkMeatContent() {
if(this.meat) {
return "this is not vegan"
}
else {
return "no meat detected"
}
}
}
Figure 6-6 VeganHotSauce contains all the top-level properties from HotSauce at the top
level too. Methods for VeganHotSauce are contained within its prototype, and methods from
HotSauce are in its prototype’s prototype
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.
checkMeatContent() {
console.log()
if(this.meat) {
return "this is not vegan.. but " +
super.getName()
}
else {
return "no meat detected.. and " +
super.getName()
}
}
}
Summary
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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_7
7. Types
Jonathon Simpson1
(1) Belfast, Antrim, UK
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.
Table 7-1 JavaScript primitive types
Operators Definition
number Any numeric value
For example:
let x = 5
string Any string value
For example:
let x = "Hello World"
bigint Any integer which is defined as bigint (has an n after the integer). Used for
creating safe integers beyond the maximum safe integer limit
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")
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.
Figure 7-1 Console logging the prototype of a wrapper object like String will give you all the
methods that can be applied to that type. All the preceding methods can be applied directly to
any string
(5).toString() // '5'
5..toString() // '5'
Number.toString(5) // '5'
The same works for numbers, which coerces numerical strings into
numbers:
isNaN(5) // false
isNaN(NaN) // true
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:
– isNaN() coerces types to numbers. For example,
isNaN("hello") will perform Number("hello") which results
in NaN, and this will return true.
– Number.isNaN() will not coerce types to numbers. It will return
false when asked Number.isNaN("hello") since “hello” does not
equal NaN.
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
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 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.
console.log(myObject[idSymbol1]) // "some-id"
console.log(myObject[idSymbol2]) // "some-other-
id"
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
console.log(myObject[idSymbol1]) // "some-other-
id"
console.log(myObject[idSymbol2]) // "some-other-
id"
Symbol.keyFor() is the opposite of Symbol.for(), and it allows you to
retrieve the text value of the symbol key:
let x = 5
if(x > 0 && x < 10) {
console.log("hello world")
}
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:
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:
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",
"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 return an error since it cannot get the property
“city” of undefined, as undefined is not an object:
if(userObject?.address?.locale?.city) {
let userObject = {
"name" : "John",
"age" : "42",
"address" : {
"houseNumber" : "5",
"streetName" : "Highway Avenue"
"locale" : {
"state" : "AZ",
"city" : "Cityopolis"
}
}
}
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 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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_8
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.
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.
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:
window.console, which we’ve used extensively in this book. It is
used extensively for logging via window.console.log, which is
also written as just console.log.
window.alert(), which creates an alert box. Although not
recommended for general use, it is still used sometimes across the
Web. An example of this can be seen in Figure 8-2.
window.crypto, used for creating random UUID values and
numbers via crypto.randomUUID() and
crypto.getRandomValues().
window.localStorage(), for accessing local storage on the
browser.
Figure 8-2 Running a method like window.alert() will create an alert box that prevents
the page from being interacted with until the user clicks OK
Figure 8-3 Console logging document or window.document on any web page will return
the same thing since they both refer to the same object. In both cases, you’ll find a
representation of the HTML document returned
<!DOCTYPE HTML>
<html>
<head>
<title>Hello World Page</title>
</head>
<body>
<p>Hello World</p>
</body>
</html>
p {
color: red;
}
<!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:
<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>
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.
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.
Figure 8-5 Using item.style.color allows us to programmatically update the text color of
HTML elements, as is shown in the preceding image
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)
Figure 8-6 You can find all methods on HTMLElements by console logging the preceding two
prototypes. Events can also be found. For example, HTMLElements have the method onclick,
which represents the “click” event
goodbyeParagraph.style.color = "green"
Figure 8-7 How our HTML page looks after applying CSS styles via JavaScript to
allParagraphs and goodbyeParagraph
<!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>
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?
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>
In this HTML, #main-text is hidden by a CSS class called
.hidden. In JavaScript, we can change if #main-text has this class
but using the toggle method classList.toggle("hidden").
When run, this method will check if #main-text has the CSS class
hidden. If it does, it will remove it; otherwise, it will add it:
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 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
})
Figure 8-8 Using everything we’ve learned so far, we can create a simple counter like the one
shown earlier
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.
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
})
Figure 8-9 When an event is performed by the user, JavaScript can either propagate the event
from the top down or the bottom up
<div id="my-div">
<div class="some-div">
<p class="paragraph">Hello World</p>
</div>
</div>
document.getElementById("my-
div").addEventListener("click"...
If the user performs an event (e.g., click) on .paragraph, we also
have to remember that it is contained within #my-div. While the user
clicked .paragraph, they are technically also clicking #my-div. So
the event you performed bubbles up from where the event happened to
#my-div, firing the addEventListener we had for #my-div.
In other words, the event the user applied to an element bubbles up
through the DOM. Similarly, if #my-div is within the body tag, then a
click event assigned to the body will still fire if #my-div is clicked. As
such, the event bubbles up from where it happened, until it hits the
item with an event listener. This is illustrated in Figure 8-10.
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
document.addEventListener("click", function(e) {
e.stopPropagation()
})
There is also a method called e.stopImmediatePropagation,
which has a similar name but is slightly different. Not only will
e.stopImmediatePropagation stop further bubbling of an event upward,
but it will also stop any further event handlers of that type from being
added to the element. For example, if you used
e.stopImmediatePropagation within a click event for an
element, any other click events added to that element would not work.
Note: Some events do not bubble, and these are listed below:
scroll – Since you only ever scroll in one element, and a scroll
should not cause another scroll somewhere else.
load – Since you tend to apply this event to check if certain elements
have loaded – for example,
document.addEventListener("load"), to check if the DOM
has loaded.
focus – Since you focus on only one element. This event is fired, for
example, if you focus in on a form element.
blur – The opposite of focus, so the same applies as for focus.
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:
<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:
The click event is performed by the user.
JavaScript starts from the top down. For example, the browser would
apply click events to html → body → #my-div.
In bubbling, the order would have been .paragraph → .some-
div → #my-div. JavaScript goes down the DOM to find the element.
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:
once, which if set to true will cause an event to only fire on the first
attempt. By default, it is set to false.
passive, which prevents the behavior of e.preventDefault().
By default, it is set to false – except for wheel, mousewheel,
touchstart, and touchmove on non-Safari browsers, where it is
set to true.
The first two options are straightforward given what we’ve
discussed already, but the last requires some explanation.
First of all, e.preventDefault() is another method found on
the e variable. It prevents the default behavior that an event would
cause upon an HTML element, for example, e.preventDefault()
applied to click events on check boxes, preventing those check boxes
from being checked. Similarly, when applied to links, it stops those links
from being clicked. In the following code, for example, we use this
function to prevent all links on a page from being clicked:
for(let x of anchors) {
x.addEventListener('click', (e) => {
e.preventDefault()
})
}
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 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.
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.
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 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;
}
Note We’ll be using relative positioning to move the div. As such,
it’s important we set a default left and top value so that we don’t
create a NaN value in our code.
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 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
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.
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
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)
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:
HTMLElement.prepend() – For adding a new HTML element
directly inside the HTMLElement, at the start
HTMLElement.append() – For adding a new HTML element
directly inside the HTMLElement, at the end
HTMLElement.before() – For adding a new HTML element
before a specified element
HTMLElement.after() – For adding a new HTML element after a
specified element
HTMLElement.innerHTML – For directly changing all the inner
HTML of an element into something else
To demonstrate how this works, imagine we have some HTML that
looks like this:
<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>
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 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"
document.documentElement.style.setProperty('--my-
background-color', 'red')
document.documentElement.style.setProperty('--my-
text-color', 'white')
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”:
Figure 8-14 The getBoundingClientRect function is another powerful tool for getting
information on the current position of any HTML element
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.
We’ve also learned how to create HTML elements programmatically
via JavaScript- and how to change CSS too. Finally, we’ve looked at how
you can find the location of any HTML element on a page using specific
HTML element methods.
This chapter has covered all of the basics which you’ll need to get
started, but there is so much more to learn beyond this. The best way to
learn is by experimentation! Using the examples we’ve covered in this
chapter, why not try building your own interactivity into web pages?
Try playing around in your web browser’s console, in your own
JavaScript files, or via online tools like jsfiddle.net. You can expand on
these examples quite easily. For instance, you could turn the drag-and-
drop example into a fully fledged whiteboard. As you start to explore
what’s possible, you’ll learn more and become even more familiar with
the concepts we’ve covered here.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_9
While objects and arrays are the most common way of storing data
in JavaScript, they do have some limitations. For example, neither
objects nor arrays have the ability to create a unique list without
duplicates. Similarly, objects cannot contain keys that are not strings
or symbols. To overcome these challenges and more, two additional
special types of objects exist known as maps and sets. Both of these
have unique functionality, which makes them well-suited to certain
tasks. In this chapter, we’ll be diving into both of these objects and
when to use them.
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:
// 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:
Set.add() – To add an item to your set
Set.delete() – To delete an item from your set
Set.clear() – To clear all items from your set
In the following example, we utilize all three methods to create a set
that contains only the value 5 before clearing it to empty:
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
example. Duplicates will automatically be removed if any exist. You can
see how this looks in Figure 9-1.
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:
let mySet = new Set()
mySet.add(4)
mySet.add(5)
mySet.add(6)
mySet.forEach((x) => {
console.log(x) // 4, 5, 6
})
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:
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:
Maps have no prototype – While map objects do inherit a prototype,
the map itself contains no keys unless we insert one into it. In
JavaScript objects, we have to use Object.hasOwnProperty() to
find out if a property is from the object itself or its prototype. Maps
do not search the prototype, meaning we know maps will only have
what we explicitly put into them, and will not inherit other
properties.
Maps guarantee order by chronology – They are guaranteed to be
ordered by the order they were inserted. Since maps use a specific
set() method to create new entries, the order of entries
corresponds chronologically to when set() was used.
Maps allow anything to be set as a key – The keys of a map can be
anything, including a Function or even an Object. In objects, it
must be a string or symbol.
Maps have performance benefits – They have better performance
than objects on tasks that require rapid or frequent
removal/addition of data.
Maps are iterable by default, unlike objects.
Similar to with sets, a map is initiated using the new Map()
constructor:
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.
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.
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"))
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.
This means that if you do what we did in the preceding example
enough, you might run into memory issues, and ultimately memory
errors in your code.
To overcome that particular problem, another type of map called a
WeakMap exists. WeakMaps have a more limited set of methods (get,
set, has, and delete) but will allow for garbage collection of objects
where the only reference to that object is in the WeakMap. WeakMaps
are not iterable, meaning accessing them relies solely on get(). This
makes them much more limited than maps but useful when solving this
particular problem. You can create a WeakMap in the same way as a
map, but by using the WeakMap constructor instead:
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'
})
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
]:
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:
let objConversion =
JSON.stringify(Array.from(myMap))
// Returns [["some","value"],
["someOther","value"],["aFinal","value"]]
console.log(objConversion)
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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_10
Figure 10-1 APIs allow for communication between servers and web pages. Sometimes, APIs
may be built into the browser and allow communication between the operating system and a
web page, such as with Web APIs
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.
Figure 10-2 Web pages are loaded via HTTP GET requests
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.
Each of these actions come with expected behaviour. For
example, when using GET, the expected behavior is that the server will
return a resource. For web pages, this could mean returning HTML, but
it’s also pretty common for a web page URL to return XML or JSON too.
GET is not the only type of HTTP request we can send, though. The
other methods are shown below:
GET – Meant for getting a resource
POST – Generally meant for creating a new resource
PUT – Meant for replacing entire resources or major parts of
resources
PATCH – Meant for modifying small parts of a resource
DELETE – Meant for deleting resources
CONNECT – Meant for initial connection to the URL or IP. Usually
used when there is encryption or authentication required
TRACE – Meant for looping back the response as the request, for
testing purposes
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.
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:
const express = require("express")
const http = require("http")
server.listen(3000)
npm i http
npm i express
Figure 10-3 Once Node.JS is installed, you can use terminal to install dependencies via the
npm command
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:
Note It’s relatively common for a server to store data it receives via
a POST API request. For example, there are Node.JS packages for
MySQL and MongoDB which allow you to save data sent to the server
in the server’s database. You could also store user data from a
registration form, letting you create login systems.
node app.js
{
"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:
JSON.stringify() for serializing JSON into a string (i.e. turning a
JSON object into a string).
JSON.parse() for turning a string consisting of JSON back into a
functioning JSON object.
When sending data to an endpoint (e.g., if we want to POST data to a
Node.js web server), we usually send it as stringified JSON:
JSON.stringify({
"name" : "John",
"age" : 105,
"interests" : [ "hiking", "skiing", "working"
]
})
/*
This will return a string:
'{"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.
While JSON can be considered almost identical to any normal
JavaScript object, there are some differences. When you try to stringify
or parse an object into JSON, you will encounter these limitations:
undefined, Function, and Symbol do not work in JSON. These
properties will disappear when using JSON.stringify or
JSON.parse.
Maps or sets (two data types we’ll cover in a later chapter) are
parsed to {} in JSON.
Dates are parsed to strings since Date has a .toJSON method.
Infinity, NaN, and null are all considered null.
fetch("https://google.com").then((res) => {
// Do something with response
})
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.
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'
});
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.
On the server, we can configure HTTP methods as well. In the
following example, we configure a “put” method function:
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.
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:
no-referrer – No referrer is sent.
no-referrer-when-downgrade – No referrer is sent if you are
sending from HTTPS to HTTP.
origin – Only sends the domain, and not the URL, that is, some-
site.com instead of some-site.com/some/page.
origin-when-cross-origin – Only sends the domain if the
request is cross-origin (different domains).
same-origin – Only send the referrer if on the same domain.
strict-origin – Sends only the origin for HTTPS to HTTP
requests and the whole thing for HTTPS to HTTPS requests.
strict-origin-when-cross-origin – Sends only the origin
when HTTPS to HTTP requests or when cross-origin. Otherwise, it
sends the referrer.
unsafe-url – Always sends the referrer even if it’s HTTPs to HTTP.
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 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.
resolve(true)
// ...
reject(false)
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
})
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}`)
})
When a promise is fully resolved, meaning the final promise in a
chain resolves to be fulfilled or rejected, then we can use finally()
to do any final housekeeping.
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"
})
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.
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)
})
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:
Promise.all([firstPromise, secondPromise,
thirdPromise]).then((data) => {
console.log(data)
})
Promise.all([firstPromise, secondPromise,
thirdPromise]).catch((data) => {
console.log(data)
})
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”:
Promise.race([firstPromise, secondPromise,
thirdPromise]).then((data) => {
console.log(data)
})
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:
Promise.any([firstPromise, secondPromise,
thirdPromise]).then((data) => {
console.log(data)
})
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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_11
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().
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
}
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:
localStorage.setItem("someKey", "someValue")
sessionStorage.setItem("temporaryKey",
"temporaryValue")
Both localStorage and sessionStorage store data in string
format, so an object should be stringified with JSON.stringify()
before being stored in your localStorage. Both localStorage and
sessionStorage have the same methods:
setItem(key, value) – To set a key–value pair in
localStorage or sessionStorage.
getItem(key) – To get the value of a key by the key’s name, that is,
localStorage.getKey("someKey").
removeItem(key) – To remove a key–value pair by the key’s
name, that is, localStorage.removeItem("someKey").
key(index) – To get the name of a key by the key’s index, that is,
key(0), to get the first key’s name. In our example where someKey
is the first key, then localStorage.key(0) will return
“someKey”.
clear() – To clear all data in either the sessionStorage or
localStorage, that is, localStorage.clear().
Note The Web Storage API gives us a simple way to store data on a
user’s computer, but it is quite limited. As such, another storage API
called the IndexedDB API exists to store more complex data.
Complex data storage usually happens on the back end, so you may
not find use for this, but it does exist if you have more complicated
front-end storage needs.
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()
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. 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:
The Performance Web API, used to monitor website performance
(through window.Performance)
The File System Web API, used to manipulate a user’s local file
system (through a variety of window methods, like
window.showOpenFilePicker())
The Payments API, used to request payments via browser or OS built-
in payment tools (think Apple or Google Pay) (through
window.PaymentRequest)
The Selection API, used to get the location of the caret when editing
text or get a range of text characters currently selected by the user
(through window.Selection)
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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_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()
Figure 12-1 Console tracing can be useful when you have complicated functions. It tells you
exactly which function called which and where in the code it’s found
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.
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
Instead of console logging a specific string of text, these methods
create timers, which return the time in milliseconds. If we wanted to
create a timer with the name “my timer,” we would first run
console.time("my timer") to initiate the timer. From the point
where that is called, the clock starts ticking.
Then when we want to log the time, we use
console.timeLog("my timer") using the same name as we
initiated with. When we want to stop, we use
console.timeEnd("my timer").
In the following example, we log three timers inside setTimeout
functions:
console.time("my timer")
setTimeout(function() {
console.timeLog("my timer")
}, 1000)
setTimeout(function() {
console.timeLog("my timer")
console.timeEnd("my timer")
}, 1500)
my timer: 1002.744140625 ms
my timer: 1506.8349609375 ms
my timer: 1507.031005859375 ms
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").
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.
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"],
["Mary", "Raspberry"]
]
console.table(favoriteFruits)
Table 12-1 Console.table gives us the ability to create table views inside the console
const favoriteFruits = {
"name" : "John",
"age" : 105,
"place" : "Planet Earth"
}
console.table(favoriteFruits)
Table 12-2 Console.table supports both objects and arrays
index value
name John
age 105
place Planet Earth
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.
Using a catch statement here gives us the opportunity to not only
prevent an error from causing our code to stop execution but also to set
the JSONData variable to an empty object, in case we need to
reference JSONData later.
We can generate our own errors on purpose using the throw control
statement. Usually we use this when something bad has happened in
our code, and we want to end execution. When used in a
try...catch block, it results in the catch block always being used:
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 = {}
}
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 {}
}
}
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.
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");
}
}
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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_13
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.
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:
let myCanvas =
document.getElementById("canvasElement")
console.log(context)
Drawing Rectangles
Three main methods exist for drawing rectangles:
context.rect(x, y, width, height) – Outlines where a
rectangle or square should be but does not fill it.
context.fillRect(x, y, width, height) – Creates a
rectangle and immediately fills it.
context.strokeRect(x, y, width, height) – Creates a
rectangle and immediately outlines it with a stroke.
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!
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:
let canvas =
document.getElementById("canvasElement")
let context = canvas.getContext("2d")
let canvas =
document.getElementById("canvasElement")
let context = canvas.getContext("2d")
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()
Figure 13-1 Two rectangles draw beside each other in separate colors using beginPath
Drawing Circles
Two methods exist on 2d canvas contexts to draw circles:
context.arc() for drawing regular arcs
context.ellipse() for drawing ellipses rather than circles
The arc() method has the following arguments, with the last being
optional:
let canvas =
document.getElementById("canvasElement")
let context = canvas.getContext("2d")
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 draws a line between that point and another.
Creating an equilateral triangle looks like this:
let canvas =
document.getElementById("canvasElement")
let context = canvas.getContext("2d")
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
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);
}
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.
image – The image we want to use, generated from our new
Image() constructor.
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.
cy – This is how far from the top we want to crop the image by. So if
it is 50, the image will be cropped 50px from the top side.
sw – This is how big we want the image to be from the point of cx. So
if 100, the image will continue for 100px from cx and then be
cropped at that point.
sh – This is how big we want the image to be from the point of ch. So
if 100, the image will continue for 100px from ch and then be
cropped at that point.
x – The x position on the canvas for the top left corner of the image.
y – The y position on the canvas for the top left corner of the image.
width – The width of the image. If left blank, the original image
width is used.
height – The height of the image. If left blank, the original image
height is used.
Figure 13-4 An example of how image cropping works. The image is first cropped using the
arguments cx, cy, cw, and ch. That cropped image is then painted on the canvas at positions x
and y, with a width of width and height
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:
let canvas =
document.getElementById("canvasElement")
let context = canvas.getContext("2d")
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"
Any normal text baseline is accepted. You will be familiar with these
if you’ve ever used text baselines in CSS. The accepted values are top,
middle, bottom, hanging, alphabetic, and ideographic,
where ideographic only really applies to East Asian scripts.
Similarly, we can change where the x coordinate begins drawing
from by using textAlign, which can be set to left to start drawing
from the left of the text, center to start drawing from the middle,
and right to start drawing from the far right:
context.textAlign = "center"
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:
We can use e.pageX and e.pageY to get the live position of the
user’s cursor relative to the page.
The canvas itself exists on a web page, and as such it is not
necessarily at the top left of the page. We therefore have to calculate
the user’s cursor relative to the canvas.
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.
Figure 13-5 Getting a cursor’s position within an element can be achieved by subtracting the
element’s position from the cursor’s position. That way, when the cursor is at the top left of the
element, it is add (0, 0). Previously, canvas x and y coordinates can be retrieved via
canvas.getBoundingClientRect()’s x and y properties. The cursor x and y can be
retrieved by e.pageX and e.pageY, respectively
let canvas =
document.getElementById("canvasElement")
let canvasXPosition =
canvas.getBoundingClientRect().x
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 canvas =
document.getElementById("canvasElement")
let context = canvas.getContext("2d")
let clicked
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:
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 canvas =
document.getElementById("canvasElement")
let context = canvas.getContext("2d")
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)
Figure 13-6 The canvas is the perfect place to perform animations since we aren’t burdened
by slow DOM updates. By using requestAnimationFrame(), we can also ensure a smooth
60fps animation
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.
© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
J. Simpson, How JavaScript Works
https://doi.org/10.1007/978-1-4842-9738-4_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 five minutes to run, your
single-threaded JavaScript will be frozen until that operation is completed. With
web workers, you can outsource that processing to another thread, so that it
doesn’t take up your main thread.
Note You cannot run a web worker in JS files on your computer. It needs to be
run on a server. That means you can set up your own Node.JS server as we did
in Chapter 10, or you can get a web host to host your files on.
In Chapter 5, we discussed how JavaScript can outsource calculations to Web APIs,
which process code on a separate thread, and then pass it back to JavaScript via
the event loop. Web workers work in a similar way, as shown in Figure 14-1.
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
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}`)
}
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:
The worker data is received by the worker, and it is stored in e.data. We can do
whatever we want with this data – for example, if we sent an object like this:
Oftentimes, you’ll find code triggering web workers on some kind of user-
based event, like clicking a button:
document.getElementById('button').addEventListener('click',
function(e) {
myWebWorker.postMessage({ "message" : "outcome" });
});
We could use a message from a user event in the thread.js file to trigger
some kind of processing. When the processing is done, we can send a message
back to the main thread. This can be any type of data, such as an object or string.
In our hypothetical example where our web worker is an image processor,
postMessage could send back a processed image to the main thread:
onmessage = function(e) {
if(e.data.startScript === true) {
// .. Do something
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:
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.
Figure 14-2 Web workers have their own dedicated globalThis, which is different from window. You can
test it out yourself by using console.log(globalThis)
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.
Index
A
addEventListener
Animations
Anonymous functions
Application programming interfaces (API)
advantages
canvas
See Canvas
description
HTTP
principles
REST
servers/web pages
Array.prototype
Arrays
duplicates
forEach methods
last element
length
manipulation methods
pop and shift
push and unshift
splice
NodeList
and objects
promises
Arrow functions
Assignment operators
template literals
variable concatenation
Asynchronicity
B
Back-end JavaScript
Bitmap
Block scoping
logical statements
variables
Bubble phase
C
Canvas
additional contexts
animations
API
arguments
DOM
drawing application
element
HTML
interactivity
in JavaScript
Capture phase
Circles
classDetails() function
Classes
and constructor functions
getName method
inheritance via extends
methods
private method fields
static method fields
.close() method
Code conventions
semicolons
spacing
variable and function naming
Code editor
Comparison operators
Conditional operator
Console counting
Console grouping
Console.log method
Console methods
console grouping
console tables
Console object
assertion
tracing
Console tables
Console timing
Constructor functions
arguments
classes
in JavaScript
syntactic sugar
Counter creation
CSSStyleSheets
with JavaScript
See JavaScript
variables
D
Date type
Document object model (DOM)
Drag and drop demo
Drawing shapes
circles
images
rectangles
text
triangles
E
ECMA-262
ECMAScript
Equality
algorithm
object and reference
object comparison
triple equals sign
Errors in JavaScript
errors
finally statement
try...catch
e variable
Event loop
Event types
description
drag/drop demo
event capturing, targeting, and bubbling
live data
options
pointers, touch, and mouse
F
Falsy types
Fetch function
body
cache
credentials
headers
integrity
keepalive
method
mode
options
referrer
referrerpolicy
File System Web API
Front-end JavaScript
Functions
additional methods
anonymous
arguments
with arrow notation
callback
calling with context
constructor
CSS
e variable
getters and setters
JavaScript
keyword
myFunction
node_modules
parts
sloppy mode
stack
three dots syntax
unnamed function expressions
G
Generator functions
getComputedStyle
getElementById()
getPropertyValue
Google Chrome
H
Heaps
non-object types
object and reference equality
and stack
History API
history.pushState()
history.replaceState()
HTMLCollection.prototype
HTMLCollections
HTML elements
e variable
events addition
counter creation
event types
modal window creation
HTMLCollections
manipulation after selection
NodeLists
order of
selection
HTTP
API
benefits
with JSON
endpoints testing
Node.JS web server
RESTful design
routes/endpoints
typical terminology
web pages
I
IEEE 754
If…else statements
Images drawing
IndexedDB API
Interactivity
Iterable data
Iteration
array forEach methods
for loops
objects not iterable
protocol
string
and values
J, K
JavaScript
APIs
assignment operators
back-end server code
changing styles
classes
client-side code
comments
console object
See Console object
constructor functions
CSS properties
CSS variables
data types
errors and exception
See Errors in JavaScript
fetch() method
See also Fetch function
front-end server code
function in
HTML dimensions
HTMLelements
HTML file
inheritance
logical statements
loops
See Loops
map
mathematical constants
new elements
non-object value
nullish coalescing
objects
See Objects
people learning
Python
RAM
server-side code
square brackets
style sheets, HTML
support
tools
type classification
variables
wrapper objects
writing
back-end code
code editor
front-end code
JavaScript Object Notation
L
Logical AND operator
Logical operators
Logical OR operator
Logical statements
block scoping
comparison operators
conditional operator
if…else statements
logical operators
modification
switch
and variables
while clause
Loops
breaks and continue condition
for statement
labels
while loop
M
Maps
iterating, merging, and accessing
Javascript
keys and values
key’s membership
retrieving properties
serialization
Memory management
Modal window
Mutation
N
Naming functions
NaN
Node.JS project
NodeList.prototype
NodeLists
Non-mutable objects
Nullish coalescing
Number()
Number type
mathematical methods
mathematics
O
Object wrapper
Object equality
Object.keys()
Object.prototype
Objects
See also Console object
accessing
destructuring objects
dot notation vs. square brackets
additional methods
and arrays
classes
getters and setters
mutability
not iterable, default
number wrapper
SetIterator
2D drawing
value
window and document
wrapper
Optionality
P, Q
Payments API
Performance Web API
Postman
Primitive types
Primitive wrappers
Private method fields
Promise.all
Promise.allSettled
Promise.any
Promise.race()
Promises
await keyword
catch and finally methods
constructors
methods
multiple
Promise.all
Promise.allSettled
Promise.any
Promise.race()
Prototype-based programming
inheritance
object shallow and deep copies
[[Prototype]] vs. prototype (and __proto__)
Prototypical inheritance
R
Random access memory (RAM)
RangeError
Rectangles drawing
Representational state transfer (REST) API
S
Selection API
Semicolons
Serialization
Set.has()
Sets
iteration and values
keys and values
membership
merging
modifying
size
someNotification.close()
Splice
Spread syntax
Stacks
event loop
JavaScript
variables
Static methods fields
String()
Strings
data of type
HTTP request
iteration
JavaScript types
object keys
symbol
of text
URI function
while arrays
Super keyword
Switch statements
defaulting to clause
equality
Symbol.iterator
Symbol type
Syntactic sugar
T
Target phase
Template literals
Text drawing
Triangles
Truthy types
U
URL API
V
Variables
arrayLength
block scoping
concatenation
conditional operator
context
CSS
definition
destructuring objects
drag
e variable
function scope
global
logical AND operator
and logical statements
name
non-object type
setting
with const
with let
with var
type string
VeganHotSauce
without values
VeganHotSauce
Visual Studio Code (VS code)
W, X, Y, Z
WeakMaps
Web APIs
History API
JavaScript
notifications
URL API
Web Notification API
Web Storage API
window object
WebGL
WebGPU
Web Notification API
Web Storage API
back-end storage
IndexedDB API
localStorage
sessionStorage
Web workers
computation
JavaScript
messages
restrictions
sending messages
Window
HTML document
HTML elements
object
window.fetch()
Wrappers
to create types
object
primitives