JavaScript Reference
In the previous chapter, we created a very basic web page consisting of one HTML file and one CSS file. Now, we’ll turn our attention to JavaScript which is the main language we’ll use throughout this book.
Over the next four chapters, we’ll cover everything you need to know about JavaScript to follow the code in this book.
These chapters are for you:
- If you have never used JavaScript before.
- If you are familiar with older versions of JavaScript and you’re wondering what these fancy new modules and arrow functions are all about.
- If you’re making the switch from another language like Python or Java.
- If you’re already familiar with JavaScript but you want a refresher on the parts we’ll be using throughout the book.
These chapters are intended to be a lighting quick introduction to bring you up to speed so you can proceed with this book, no matter your background. If you do want to go deeper on any topic, the excellent and in-depth tutorials on the Mozilla Developer Network (MDN) are a good place to start.
In this chapter, we’ll start by exhaustively (and perhaps exhaustingly) exploring the syntax you need to know to follow the main text. You may find it more useful to use it as a reference rather than attempting to read through from start to finish. You will be referred here from the main text whenever we encounter a new JavaScript feature.
This chapter is not a complete JavaScript reference. The JavaScript used in this book is only a fraction of the entire JavaScript API, we make no attempt to explain prototypical inheritance, and we’ll only briefly touch on scope and closures which are arguably the trickiest parts of JavaScript to understand.
Old-School and Modern JavaScript
Throughout this section, we’ll frequently refer to old-school and modern JavaScript. Here, modern means any features added to JavaScript in version ES6 (released in 2015) or later. Old-school refers to any syntax that existed prior to that, from versions ES1 through ES5. The reason for this distinction is that when using a feature of modern JavaScript, you have to take care to ensure it has been implemented by browsers, although fortunately this is becoming less and less of an issue as browser vendors catch up with the spec.
Modern JavaScript is also referred to as ESNext. All the syntax from old-school JavaScript still works, so we can consider ESNext to be a superset of old-school JavaScript.
JavaScript Modules and the Entry Point
All the JavaScript we write will go inside files with .js
extensions. Rather than put everything into one file, we’ll split our code into many small modules, a topic to which we devote
an entire chapter since modules are part of modern JavaScript and may be unfamiliar to many people.
Once we’ve split up our application in this manner, we’ll have one main JavaScript file that references and coordinates the other JavaScript modules. This main file is called the entry point of our application, and we will name it main.js (it’s also common to call this index.js or app.js).
Referencing JavaScript Modules from HTML
Next, we need to connect main.js to our minimal HTML page so that it will run when the page loads. We touched on this in the previous chapter.
Here, we have placed the main.js file in a folder called src/ right next to the index.html file.
To load this file, we’ll add a <script>
element with a src
attribute to
the head section of index.html. The src
attribute will reference main.js.
Open up the inline code editor on this page to see this in action.
This file also has a type="module"
attribute to let the browser know that we’re splitting our code up into modules.
Inline Scripts
You can also write JavaScript directly in HTML like this:
Inline scripts can also have the type="module"
attribute:
We’ll never write inline code in this book. It’s cleaner to keep your JavaScript in a separate file.
However, to the browser, there’s no difference between code written inline or in a separate file. Any code from this chapter will work the same way in either case.
The Developer Console
Yet another place you can run JavaScript code is the browser console. Press F12 to open it now, if you’re reading this from a device with a keyboard.
The console provides a handy scratchpad for testing out ideas while you work on a website. If you like, you can open the console now and test out the code in this chapter while you read.
Comments
Single line comments in JavaScript start with a double forward slash: //
We can also write multi-line comments, starting with /*
and ending with */
.
Keywords
In general, you can name things whatever you like when writing JavaScript. You can even use emojis or hieroglyphs. However, some keywords are reserved. These are used to access functionality of the language, for example var
, let
, const
, function
, Object
, String
, Number
, class
, this
, and so on. We’ll encounter many of them throughout this chapter.
Operators
Most special characters that can be typed using a standard keyboard are reserved as operators, for example +
, -
, *
, /
, %
, !
, (
, )
, and so on. We’ll cover many of these in more detail below, and
this page has a complete list.
Capitalization Matters
The capitalization of a word matters in JavaScript. This applied both to reserved keywords and names you create. For example, String
is a reserved keyword, but string
is not. If you create an object called Cat
, it will be different from an object called cat
.
Variables: let
, const
, var
Variables are named areas of the computer’s memory. We can save some data (a value) to a variable and then use the name to access it later. In JavaScript, variables don’t have a specific data type, so we can save any type of data such as strings, numbers, arrays, objects, and functions, to any variable.
Variable names cannot start with numbers. 23cats
is not a valid variable name, but cats23
is.
Variables names can start with some special characters (as long as they are not used for operators). The two non-reserved special characters that can be typed with a standard keyboard are $
and _
, so variables names like $cats
, _cats
are common.
Naming Conventions
While the JavaScript spec doesn’t include any special rules for naming things beyond the ones described above, in practice certain conventions are often used. For example, when a function name starts with a capital letter (Cats
), it is usually a class or a constructor (more on that below), while a completely capitalized word is often used for mathematical constants (EPSILON
, PI
, SQRT2
, and so on). Variables names that start with _
are usually intended to be private (not accessible from outside the current scope, module, or file).
Camel Case
Variable names are usually written in
CamelCase: aHerdOfCats
, aKindleOfKittens
.
var
Old-school JavaScript had one way of defining a variable, using the keyword
var
:
var
has some technical issues which we won’t get into here, so the ES6 release of JavaScript introduced two new ways of defining variables: let
and const
.
You can still use var
for backward compatibility, but you don’t need to anymore, and we’ll never use var
in this book.
const
const
is used to assign data to a variable that cannot be changed within the current scope (
we’ll explain scope below).
Attempting to change it later will cause an error:
let
When using
let
to define a variable you are saying that the value may be changed sometime in the future:
Unlike with const
, you can change the value later:
const
will always be our first choice for assigning variables. We’ll only use let
if we are certain that a variable needs to change later.
Aside from any optimizations that the JavaScript engine might perform, it’s useful to look at code and know at a glance if a variable will have the same value everywhere, or might change later in the code.
Dynamic Typing
JavaScript is a loosely or dynamically typed language. In short, this means that we can assign any data type to a variable:
This makes the language flexible, but it can also lead to confusion as it puts the responsibility on you, the programmer, to remember what kind of data a variable holds.
One of the benefits of const
is that you know a variable will always hold the initial data type that you assigned to it.
Primitive Data Types
Until recently there were only five
primitive data types in JavaScript: Boolean
, Number
, String
, Null
, and Undefined
.
Two new types, Symbol and BigInt, were added recently. We won’t need to use those in this book though. Let’s examine the other five now.
Boolean
Booleans are the keywords true
and false
. We can use them as flags to let us know whether something is switched on or not, has been completed, needs to be updated, and so on:
Notice that the variables we have saved these Booleans into are all declared using let
. Since they are flags, we’ll usually want to change the value at some point. For example, when we start to play the animation we’ll set animationIsPlaying
to true
, when we switch off the light we’ll set lightIsOn
to false
, and so on.
Booleans also returned by the comparison operators which we’ll introduce in a few moments.
Number
JavaScript does not differentiate between an integer such as -23, 0, or 100 and a floating-point number such as 0.05, 23.0002, or 4.5. All numbers are stored as floating points. There’s no such thing as an integer value in JavaScript. In other words, 5 and 5.0 are the same value. This is not the case in every programming language.
All numbers are represented using the Number data type. The Number data type can hold any number between $-2^{53} - 1$ and $2^{53} - 1$ ($-9007199254740991$ and $9007199254740991$).
Special Numbers
There are a couple of special constants within the Number data type that you might encounter:
Infinity
,
NaN
(Not a Number), and
Number.EPSILON
which represents the smallest possible value that can be represented in JavaScript.
Working with Floating-Point Data
All numbers in JavaScript are floating-point numbers, and as with any programming language, care must be taken to avoid floating-point errors.
Some decimal numbers cannot be represented accurately in binary, and this results in a common class of errors called floating-point errors.
For example, $12.9 \times 2.3 = 29.67$, right?
Not according to floating-point arithmetic!
We expect $29.67$, but we get $29.669999999999998$. This is an error of approximately $0.000000000000001$, or $1 \times 10^{-15}$.
Of course, is an exceedingly tiny number. In practice, in most cases, you won’t notice this error.
However, there are some situations where this is a problem:
- Comparing the results of two mathematical operations together.
- Using the result as input for another calculation.
In the second case, if you perform many thousands of calculations, which is common when performing animations, for example, the error will increase with every new calculation. Within a couple of seconds, you may notice your animations have gone out of sync.
This is a somewhat complex topic and we won’t get into it more deeply here. If you want to look explore this further, here’s a simple article, and a slightly less simple article with some techniques for resolving the problem.
String
The String data type represents arrays of letters, numbers, and other characters as well as methods for manipulating them.
Strings which can be defined using single quotes:
Or using double quotes:
Escape Characters
When using double quotes we can write the word "I'm"
which contains a single quote character.
To write this using single quotes, we have to use a backslash
escape character:
Template Strings
A new way of creating strings became available in ESNext JavaScript, known as template literals or template strings. These are defined using backticks rather than quotes, and allow us to do a few extra things. For example, template strings can span multiple lines:
We can also use template strings to include variables and calculation in our strings, by placing them inside ${}
.
This makes combining and adding strings together much simpler. To display the result of adding two numbers in old-school JavaScript we’d have to do all of this:
Phew!
Doing this with template strings is much simpler:
Null and Undefined
The next two primitive data types, Null and Undefined, are similar in JavaScript. Both mean “nothing here”.
The main difference is that undefined
get assigned automatically whenever some value is not found, whereas null
only happens when the programmer types in null
somewhere in the code.
If you ask JavaScript “what is x
?” but you’ve never given any value to x
, then it will tell you “x
is undefined
”.
By contrast, you might set let x = null
somewhere in your code to let yourself know that x
has not been given a value yet.
Note that the name of the data types are Null and Undefined, starting with capital letters, but when we use them in our code, they start with small letters: null
and undefined
.
Objects
Object is the only non-primitive data type in JavaScript. Non-primitive means that objects are made up of collections of other data types.
Objects in JavaScript are defined using curly braces: {}
, and hold collections of data in [key, value]
pairs.
const object = {};
Everything that is not one of the above primitive data types is an object, including functions, arrays, and classes. This makes Object the most important data type in JavaScript, and you should take the time to understand it well. For more details check out the Working with objects guide on MDN.
When we create a function, array, class, or any other entity that derives from object, we immediately have access to all the properties, methods, and other functionality of objects.
The object
we created above is not very interesting, since it doesn’t contain any data. Here, we are creating an object called newObject
with two keys x
and y
which hold values 5
and 'hello'
respectively.
Keys are also referred to as property names, and values are also referred to as properties. That means that x
and y
are property names, and 5
and 'hello'
are properties.
Keys/Property Names
Property names are always strings, whereas properties can be any data type (numbers, strings, arrays, other objects, functions, null
, undefined
, etc. ).
However, we don’t need to put quotes around the property names, as we usually do when defining string - they are implicit. But you can add quotes if you like:
Also, if you want to add certain characters like spaces or -
to your property names, you will need to use quotes:
If we wrap hello-kitty
in quotes, we can use it as a property name:
We can store functions and arrays in objects:
When a function is stored in an object like this we’ll refer to it as a method.
More on arrays and functions below.
Accessing data in Objects: Dot Notation and Bracket Notation
The first and most common way of accessing data in objects is to use dot notation:
You will often see methods being called using dot notation. For example, to call the printMeow
method:
Alternatively, we can use bracket notation, in which case we will pass in the key as a string.
Bracket notation is used when you need to use a variable to access the data:
You also need to use bracket notation to access property names with weird characters like our hello-kitty
example above.
Attempting to access that property name using dot notation will cause an error:
Bracket notation to the rescue!
Adding and Changing Object Data
We can add more data to an object after we have created it, and we can also change existing data. Once again, we can use either dot or bracket notation to do this.
Here, we add a key/value pair to an object using dot notation:
Note that, even though we defined the cats
variable using const
, we can still change data inside the object. However, we cannot assign a completely new value to cats
:
cats = 6; // Error! Can't update constant variable!
The delete
Operator
You can also delete a key from an object using the delete operator:
Arrays
Arrays in JavaScript are defined using square brackets: []
and hold a list of values. These values can be things like numbers, strings, other arrays, other objects, functions, and so on. Anything that can be stored in an object can also be stored in an array (since arrays are derived from objects):
We access data from an array using an index, starting at zero. In the above array, index zero holds the string 'hello'
and index three holds the number 23
.
We use bracket notation to access elements of an array at a given index:
However, we cannot use dot notation with arrays:
Arrays are Objects
As we mentioned above, everything that is not a primitive data type in JavaScript is an object. That means that arrays must be objects. Array indices must be object property names and array values must be object properties.
This also means that the indices must be strings. Let’s test that:
Sure enough, array indices are strings. When we access an array using a number, for example myArray[0]
, the quotes are added automatically for us.
Arrays have several methods to help with accessing or modifying the data they contain. Check out the Arrays page on MDN for a complete list.
We’ll use Array.push
quite a bit to add new items to the end of the array:
We can also find the length of the array using Array.length
:
We can access the last element in the array using length - 1
as the index:
The -1
is needed since arrays are indexed from zero, not one.
Functions
Functions are defined using the function
keyword and look like this:
This is an anonymous function, meaning that it has no name, and therefore no way for us to refer to it later.
Naming Functions
Often it will be useful for us to give our functions a name so that we can pass them around in our code and use them later. We can name functions in two ways. First, we can assign the function to a variable:
Second, we can directly name the function:
Calling Functions
We can call or invoke a function using either its name or the name of the variable that we assigned it to, followed by ()
. There’s no difference in either case.
Empty Functions
Functions can be empty:
This function does nothing, but it’s often useful to write functions like this and fill them in later. By doing this, we can build up our code structure before we have fully worked out how a function will work.
When we create an empty function that we intend to replace later, we’ll refer to it as a placeholder function.
Function Parameters (and Arguments)
Function can take parameters:
Later, we can call this function with two arguments. Here the arguments are the numbers 1 and 2:
a
and b
are parameters, and 1
and 2
are arguments, but this distinction is not usually important and many people use the two terms interchangeably.
The return
Keyword
The add()
function above is not very useful. It does add two things together, but it doesn’t give us any way to see the results! Usually, we want a function to give us back some data, and for that, we need to use the return
keyword.
A function will immediately exit when it encounters the return
keyword and nothing else in the function will be processed:
In this function, we are not returning any data, which means the result of the function will be undefined
:
There’s nothing wrong with doing this. Often, you want a function to exit without returning data.
Pure and Impure Functions
Another way of getting data out of a function is to change the value of a variable defined outside the function, as in this somewhat contrived example:
When we change data from outside the function, we call the function impure. In most cases, you should avoid doing this as it makes your code harder to read.
Usually, it’s easy to convert an impure function into a pure function.
Functional programming is a programming style that requires you to only use pure functions. In this book, we’ll take a pragmatic approach. We’ll aim to write pure functions but if it seems like we’re jumping through hoops to do so, we’ll allow a couple of impure functions to slip in.
Passing Data into Functions
Functions in JavaScript are polymorphic, meaning that they don’t care what kind of data type you pass in.
Take a look at the add
function again.
We can call this function with any data type.
For example, we can pass in numbers as we did above:
Or, we can pass in two strings:
Or a string and a number:
We can even add an object and a string:
JavaScript doesn’t care. But look what happened we added the number "2"
and the string "asparagus"
. We got the string "2asparagus"
since the number 2
was automatically converted to a string.
Even weirder, when we added the object and string together we got the string "[object Object]goodbye"
! Huh?
What we have encountered here is type coercion. In short, when we tell JavaScript to add two values together, the values get converted to a data type suitable for addition. For mathematical operations like addition, the end result will be either a number or a string. We’ll look at type coercion more deeply when we’re exploring comparison operators below.
The important thing to take away here is that JavaScript will let you add anything to anything, but that doesn’t mean you will always get a sensible result! Unless you know what you are doing, only add numbers to numbers or strings to strings.
What about that "[object Object]goodbye"
? In this case, once again the object has been converted to a string. When an object gets converted to a string, it becomes the string "[object Object]"
. Mystery solved, for now.
Functions Are Objects
Just like arrays, functions are also objects. This means that
everything we wrote about objects is true for functions as well. For example, we can create a new property ([key, value]
) pair on a function:
Arrow Functions
Arrow Functions are a new kind of function available in modern JavaScript. These are similar to normal functions but have a shorter syntax.
Here’s an anonymous arrow function:
Compare that to an anonymous normal function:
Let’s create a subtract function to complement our add function above, written in arrow style:
Leaving out the curly braces makes the return
keyword implicit:
This makes arrow functions extremely terse. When writing JavaScript you will create a lot of simple functions and writing them as arrow functions makes the code much shorter and easier to read.
Let’s compare that to our add
function again to show the difference:
The Difference Between Arrow Functions and “Normal” Functions
When writing simple functions like add
and subtract
, there’s no difference between arrow functions and “normal” functions. In most cases, you can pick whichever style looks best. Sometimes, writing out function...
will break up the flow of your code, while at other times you may want to make it very clear that a block of code is a separate function. We will always write anonymous functions as arrow functions.
However, in other situations, there are important technical differences between these two function styles. Understanding these requires an understanding of
scope and the this
keyword so we’ll come back to this below.
Arithmetic Operators
All the standard mathematical operators (AKA
arithmetic operators) are available in JavaScript. You can think of these operators as shorthand for functions, so the +
operator is equivalent to our add
function:
Operators come in three forms: unary operators, which take one argument, or operand, binary operators, which take two operands, and ternary operators, which take three operands. Actually, there’s only one ternary operator, all the rest are unary or binary. Note that the word binary here has nothing two do with the binary number system.
Binary Arithmetic Operators
Basic Arithmetic
These are all binary operators, which means that they work with two operands. In the binary operation $1+2$:
- $+$ is the operator
- 1 and 2 are the operands
The Modulo Operator
The modulo or remainder operator is another binary operator that gives the remainder when the left operand is divided by the right. It always takes the sign of the left operand:
The Exponentiation Operator
The final binary operator is the exponentiation, or to the power of, operator:
Unary Operators
By contrast with binary operators, unary operators work with a single operand.
First, there’s unary negation, which means take a variable and make it negative.
Of course, that means we must also have unary addition:
Unary plus is a different beast, however. It’s useless as a mathematical operator, but it turns that it’s the fastest way to turn something, such as a string, into a number:
It doesn’t work in all situations, and in many cases will return
NaN
(not a number).
Unary negation and unary addition use the same character as binary negation and addition. The only difference is whether you supply them with one or two operands.
Increment and Decrement Operators
Next in the list of unary operators are increment (++
) and decrement (--
):
The two statements return x
and then add one to x
and add one to x
and then return it are potentially confusing, so let’s see if we can make that clearer with an example.
First, let’s try x++
:
Next, here’s ++x
:
Assignment Operators
The basic assignment operator is represented by a single equals sign:
We’ve already been using this quite a bit throughout this chapter.
The rest of the assignment operators are shorthand for each of the binary arithmetic operators:
Logical Operators: AND, OR, NOT
There are three
logical operators in JavaScript: AND, OR and NOT, which are defined using &&
, ||
and !
, respectively. AND (&&
) and OR (||
) are binary operators while NOT (!
) is a unary operator.
We will often use these to compare Boolean values (true
and false
):
However, more care needs to be taken when comparing other values. We’ll discuss this in more detail in the section Truthy and Falsy below.
Comparison Operators
Comparison operators compare two values and return a Boolean value (true
or false
).
All comparison operators are binary operators, meaning they take two operands.
Special care must be taken, not only in JavaScript but in any programming language, when comparing two floating-point numbers together. See the section Working with Floating-Point Data earlier in this chapter.
Equality Comparison Operator
The first comparison operator we will examine is the equality operator. This comes in two flavors in JavaScript:
- the strict equality operator, denoted by three equals signs (
===
) - the loose equality operator, denoted by two equals signs (
==
)
To save some time here, we won’t get into a discussion of the difference between these two. If we were to discuss it, we’d realize that in nearly every situation, we should use strict equality (===
). We’ll never use the loose equality operator (==
) in this book.
You can read more about these two operators here.
Using the strict equality operator, we compare two values (operands) and we get back either true
or false
:
Inequality Comparison Operator
Strict inequality is denoted by !==
(NOT equal):
There is also the non-strict version: !=
, but again, you should avoid using that unless you have a specific reason for doing so.
Relational Comparison Operators (Greater/Lesser Comparison)
There are four relational comparison operators:
- greater than (
>
) - greater than or equal (
>=
) - less than (
<
) - less than or equal (
<=
)
There’s no need for strict and loose versions of these operators.
These all read from left to right, so when we write x < y
we are asking “is x less than y?":
Comparing Different Data Types
What happens when we compare two different data types? 6 <= 5
has an obvious answer (that’s false
, by the way), but what about 'apples' <= 5
?
Even comparing two values of the same data type doesn’t always have an obvious answer:
JavaScript allows us to compare anything with anything, and always has an answer ready. Figuring out what the answer will be is sometimes tricky though.
When writing JavaScript, it’s your responsibility to make sure that your comparisons make sense. Comparing 'apples'
with 0xffa500
is probably a sign there’s a mistake in your code somewhere.
However, we should still take the time to understand the algorithms that JavaScript uses to calculate a result when comparing different data types.
Note: for actual comparison of data types, you should use
typeof
or instanceof
, which are covered in detail below.
Type Coercion
The first thing JavaScript does when comparing different data types is to convert both values to the same primitive data type. This process is called type coercion.
Note that this coercion only happens for the sake of the comparison. The actual data you have stored in a variable is not changed in any way.
Comparing Strings with Numbers
We’ll examine the case of comparing strings with numbers in detail here. Something similar happens whenever you compare any two values of different data types.
If we compare a number with a string, then both values will be converted to a number.
This is also where the difference between strict and loose equality becomes apparent:
Usually, you wouldn’t consider the string '2'
to be equal to the number 2
. That’s why we use strict equality (===
).
It’s obvious how you can convert/coerce the string '2'
to the number 2
. But what about a string like 'asparagus'
?
Every string that is not a number or the empty string will be converted to
NaN
(Not a Number), and any number we compare with NaN
will return false
:
The empty string (''
) is a special case and will be converted to 0
:
Other Operators
We’ve covered quite a few operators here, but these are by no means an exhaustive list of all the operators available in JavaScript. Check out Expressions and Operators page on MDN for a complete list.
You may also want to check out the page on Operator Precedence which goes into some details about what happens when you are applying multiple operators in one statement.
typeof
and instanceof
Often, you will need to check what type of data a variable contains, such as strings, numbers, Booleans, functions, objects, and classes. For this purpose, JavaScript provides two similar operators:
typeof
and
instanceof
.
typeof
is the simpler of these, and you can use it to check primitive data types:
Undefined
(but notNull
)- Boolean
- Numbers
- BigInt
- Strings
- Symbols
- Functions
Using typeof
, you can easily check whether two variables contain the same primitive data type:
So far so good. However, if you use typeof
with anything that’s not in that short list of primitive data types, the result will be simply 'object'
.
As we have mentioned several times throughout this chapter, everything that is not a primitive data type in JavaScript is an object, so this answer is correct. However, it’s not very useful and it means we can’t use typeof
for comparing custom objects and classes.
For these we’ll turn to instanceof
.
Comparisons using instanceof
are a little more verbose than typeof
:
There’s quite a bit more to instanceof
since it works by checking the object’s prototype. We’re avoiding the discussion of prototypes in this chapter, so we’ll cut this section short here. As usual,
the relevant MDN page has a lot more details.
Control Flow
The term Control Flow refers to various methods of controlling what your program does in a given situation.
Block Statements
The basic element of control flow is the humble
block. These are defined using curly braces {}
, and are used to group a set of statements.
It’s unusual to see a block statement on its own like this. Usually, they are combined with other statements such as if…else or loops.
if…else Statements
The if…else statement is a conditional statement that can be used to create branches in your code.
We use comparison operators to create the branches in an if…else statement.
This example controls a loading bar. As the value of percentLoaded
increases, the loading bar is drawn a different color. Finally, once percentLoaded
reaches 100, the bar is hidden and the animation is started.
Each branch in this if…else statement is wrapped in block statement.
In this example, we check whether a Boolean value is true or false:
Since there is only one line of code in each branch of this example, you could leave out the block statements:
However, it is considered best practice to always include the block statements since that makes your code more readable.
There are more sophisticated methods of error handling available in JavaScript, but often, a simple if…else statement is all you need.
Other ways of controlling the flow of your program are the ternary operator and switch statements.
The control flow and error handling guide on MDN goes into more detail on this topic.
Truthy and Falsy
Take another look at the line where we checked whether the model has loaded above:
We can shorten this by omitting the === true
:
By doing this, we have placed modelLoadedOK
into a so-called Boolean context.
Putting a variable in a Boolean context means we are interpreting that variable as either true
or false
.
Since modelLoadedOK
is already a Boolean, that’s not a big deal. However, we can put any data into a Boolean context. For example, instead of creating a Boolean to track whether the model has loaded, we can simply use the model itself:
This is another example of type coercion, by the way.
If the model exists, then we will interpret the variable model
as true
. If the model does not exist, then we will interpret the variable as false
.
Every value in JavaScript can be interpreted as either true
or false
.
When a value is interpreted as true, we say that it is truthy.
When a value is interpreted as false, we say that it is falsy.
Nearly everything is interpreted as truthy in a Boolean context. Here’s a complete list of falsy values in JavaScript:
- The Boolean
false
- The number
0
or0-
- The empty string “”, ‘’, or ``
null
undefined
- Not a Number:
NaN
BigInt
zero:0n
And that’s it - everything else is truthy.
Loops and Iteration
One of the things that computers are good at is doing lots of similar things, really, really fast, such as adding a million numbers together.
JavaScript has several methods to perform iteration, but in general, we’ll stick with just two: for loops, and for…of loops.
For Loops
We can perform an operation multiple times using a for loop, which looks like this:
If you open up the browser console and paste in that code, you’ll see the output 0, 1, 2, 3, 4.
The rest of the loop is made up of an initialization statement, a condition, a final expression, a block, and a statement:
Everything in square brackets is optional. The {}
is also optional, but, as with the`if…else statement, we’ll always include it. Matching up the values in the square brackets to our example above, we have:
- [initialization]:
let i = 0
- [condition]:
i < 5
Here’s another simple example that adds the number 1 to total
repeatedly until the loop ends after five thousand iterations.
for…of loops
Next up is the for…of loop, which was recently added to JavaScript (it’s an ESNext feature).
for..of loops can be used to loop over the values of any iterable object.
When you hear iterable object, think something similar to an array.
Of course, that means arrays themselves are iterable objects. We can loop over all the values in an array using a for..of loop:
If you paste that code into the console, you’ll see the output 1, 2, 3, 4. The useful thing here is that we don’t need to care how big the array is. The for…of loop will hit every value, whether there are four or a million elements in the array.
In this example, we add up all the values in an array to get the total.
Iterable Objects
Many other built-in objects besides arrays are iterable. For example,
strings,
TypedArray
,
Map
,
Set
are all iterable. You can also create custom iterable objects, although that is beyond the scope of this chapter
Iterating Over Object
A notable exception from the list of iterable objects is
Object
:
It’s common to have an object containing values that you want to iterate over. Object
has several helpful methods for this purpose.
Object.values()
First up is Object.values() which simply returns the values of an object as an array.
Now we can iterate over the weightValues
array using for..of as usual:
We will usually write this more concisely by placing the call to Object.values
inline, within the body of the for…of loop:
Object.keys
Object.keys
is similar to .values
, except that it returns the keys as an array.
Object.entries
Finally,
Object.entries
returns an array of [key, value]
pairs.
Note that catEntries
is a nested array, that is, an array that contains other arrays within it.
Array.forEach
Coming back to arrays, another way of looping over the values in an array is to use the built-in method,
Array.forEach
.
This takes a function as the argument, and we can perform operations on the array elements, one by one, inside that function. In case it’s not clear, here is the function:
We can use Object.values
with Array.forEach
to loop over an object’s values, just as we did with for…of:
There are several related array methods that can be used for iterating over an array’s values:
Appropriate use of these functions can result in beautiful, concise code. In this book, we are more interested in clarity and simplicity, so we’ll avoid using them. We’ll even avoid using Array.forEach
and stick exclusively with for loops and for…of loops.
Callback Functions
The function we passed into the arr.forEach
method above has a special name: it’s called a callback function, meaning it gets passed into another function as an argument:
Let’s rewrite our forEach
loop to get a better look at this callback function:
Now you can see that we’re passing in the function called total
as an argument to Array.forEach()
. There’s nothing special about the total
function. It’s a normal JavaScript arr function. It’s only when used in this manner that we refer to it as a callback function.
We will use callback functions a lot while writing JavaScript code. We’ll cover them in more detail in the chapter on Asynchronous JavaScript.
Recursion
A recursive function is simply a function that calls itself.
Here’s a function that prints the string 'Hello'
to the browser’s console one hundred times.
Whenever you write recursive functions, it’s important to include a final case to end the recursion. Here, that’s if (count < 100)
.
If we leave that out, we’ll end up with a function that keeps on printing hello until the JavaScript engine eventually kills it with an error message.
We’ll used recursion to generate a stream of frames when we add animation to our app.
Classes and the new
Keyword
We can create instances of a class of objects using the new
keyword.
What does that mean?
Think of a particular cat called Geronimo. Geronimo is a big cat. You might even say he’s a chonker. He’s not much of a guy for friendship but he sure does like to sit on the windowsill and catch the evening sun.
Now, imagine another cat called Gemima. She’s small, friendly, tabby. She loves to meow and when she’s not chasing a toy she is probably purring on your lap.
Now think of the realm of all possible cats. Big cats, small cats, tabby, short-hair, Persian, some are friendly, some aloof, some purr, some meow a lot, while others like to be silent.
All of these cats belong to that abstract class of things we call Cat.
Geronimo and Gemima are instances of the Cat class.
Similarly, we can create a Cat
Class
in JavaScript.
This simple example class stores only two pieces of data about the cats we create: their name
and their age
.
To create an instance of the Cat
class, we’ll use the new
keyword:
If you leave out the new
keyword when creating a Cat
instance, you’ll get an error message:
Class Names are Capitalized
By convention, class names always start with a capital letter, while class instance names (and most other JavaScript variables) start with a small letter. This is not a rule, however, we will treat it as such and always start class names with a capital letter.
Classes are Objects
Remember, everything that’s not a primitive data type in JavaScript is an Object, and that includes classes.
Prototypes and Constructor Functions
Classes are a relatively new addition to JavaScript. When using old-school JavaScript the equivalent to classes is constructor functions.
We won’t get into this further here, but you will often encounter constructor functions when dealing with old JavaScript code. The Object prototypes guide on MDN is a good starting point if you want to explore the history and technical details of this part of the language
Constructor functions and classes are equivalent (everything you can write one way, you can also write the other), so we’ll use the new, cleaner Class syntax in this book.
Note that our class example above has a constructor
method inside it, which we’ll look at in a moment, but that’s not the same thing as a constructor function.
Instance Variables
this.name
and this.age
are called
instance variables and we can access them using
dot notation or bracket notation. We will refer to them as Cat.name
and Cat.age
.
Once we have created an instance of the class, we can access them using the instance name (for example, geronimo.name
).
Within the class, we can access them using this.name
and this.age
.
Class Methods
When we create a function inside a class, we refer to it as a method. Let’s add a meow
method to the Cat
class.
Once we have created a Cat
instance, we can access the method on the instance using dot notation or bracket notation:
The constructor
Method
The Cat
class we created above has a
constructor
inside it. This is a special method that runs automatically when we create a new class instance. Here, that means when we create a new Cat
.
Just to make sure we’re totally clear, we’re not talking about the old-school constructor functions we mentioned above. The constructor method is something different.
We use the constructor method to do the basic setup for a class, for example, setting up instance variables. From here on, we’ll refer to the constructor method as simply constructor.
The constructor is optional and we can create a class without a constructor if we like:
If we leave out the constructor then JavaScript implicitly adds one for us, so the above class is equivalent to:
By implicit, we mean that you don’t see this code. All the magic happens somewhere out of sight, deep within the browser’s JavaScript engine.
Returning Values from the Constructor
The return
value of the constructor is special because it’s also implicit. What this means is that we don’t need to type return
at the bottom of the constructor. Instead,
JavaScript implicitly adds the line return this
at the bottom of the function:
this
refers to the class instance created when we call new Cat
. We’ll take a deeper look at the this
keyword in a few moments.
Custom Return Values in Class Constructors
Sometimes, you may find it useful to return something other than the class instance from a class. In that case, you can add a custom return
statement to the end of the constructor.
Here’s a contrived example, where we return an object with name and age properties from the cat class:
Now, new Cat
will no longer return a class, but instead a simple object:
However, doing this means we can no longer access the Cat.meow
method.
Static Class Methods
Sometimes, you may want to use a class method without going to the trouble of creating a class instance. In that case, you can declare the method as
static
.
Now, to call the static meow method, we access it using Meower.meow
.
However, we can no longer access this method on a class instance.
Chaining Methods
Often, you’ll need to create a class and then run several of its methods to set it up. For example, here’s a frog class with some methods to set up its stats:
The stats are given default values in the constructor, and the methods allow us to customise them. Of course, for a simple class like this we could pass all the custom values into the constructor, but in real code that’s not always possible or desirable. Now let’s see what we have to do to customise all of the frog’s stats:
We can improve this slightly by using method chaining. To do this, we will return this
at the end of each method. Remember that the constructor
implicitly returns this
already.
Now we can set up our froggy using method chaining
Class Inheritance and the extends
Keyword
A powerful feature of classes is that we can take an existing class and extend it to create a new, similar class with extra or changed functionality, using the
extends
keyword.
When we do this, we call the original class the base class, superclass, or parent class, and we call the new class the derived class, subclass, or child class.
The classic examples used to demonstrate inheritance is to start with an Animal
base class, and since this chapter is already full of cats, let’s continue with that tradition.
Here’s our Animal
base class:
We’ve decided that all animals will have a single instance variable called age
, and two basic abilities: they can grow older, and they can reproduce.
Next, we’ll use the extends
keyword to create a child class called Cat
:
Cat
can do anything that Animal
can do, and they also have a name
and can meow
.
The super
Keyword
Notice the super
function. This is optional and means that both Animal.constructor
and Cat.constructor
will run when we create a new Cat(...)
:
Another option here is to completely leave out the constructor in the child class, in which case the parent’s constructor will automatically be used.
This gives us three options for the constructor in a child class:
Inherited Methods
Even though we didn’t define the growOlder
and reproduce
methods on the Cat
class, they were inherited from the Animal
base class. That means Cat
has a total of three methods. growOlder
, reproduce
, and meow
:
We can also overwrite methods in the child class. Doing so is simple: we just create a new method with the same name in the child class.
Use Inheritance Sparingly
Inheritance seems like an amazing, powerful tool, and indeed, it is, when used well. On the other hand, it’s easy to misuse and can lead to huge, ugly, and confusing towers of inherited classes.
We won’t get into this further here since this is a chapter on JavaScript syntax, not software patterns or architecture. All we’ll say for now is, if you are creating multiple levels of inheritance like this:
… there’s probably a better way to write your code (composition, perhaps?).
Some take this to extremes and try to avoid using inheritance at all. In this book, we’ll take a pragmatic approach and use it sparingly.
Another thing to consider is that three.js uses inheritance internally, and it will often be easier for us to follow the patterns of the library we are using rather than trying to work against it.
Scope and Closures
Scope is an important concept in JavaScript, but often confusing for beginners (and experts!).
Simply put, the scope of a variable means “where can I access this data from?”. If data is not in the current scope, then we cannot access it. Here, data can refer to a variable, object, function, class, and so on.
There are four types of scope:
- Global scope
- Module scope (the current file)
- Block scope
- Function scope (closures)
Module scope and block scope are new in ES6. In old-school JavaScript, there was only global and function scope.
Scopes fit inside each other like Russian dolls. When we are in a certain scope, we can access anything from any of the scopes higher up. At the top level is global scope, which we refer to as the Window
object in the browser (Node uses different terminology). The next level down is module scope. When writing modular JavaScript, each module (file) has it’s own module scope. Finally, block and function scopes can be nested inside each other to any depth.
When we are in module scope, we can access anything from the module scope and global scope. When we are inside a block or function within the module, we can access anything from function/block scope, and module scope, and global scope (we can always access global scope).
However, when we are in global scope, we can’t access module scope, and when we are in module scope, we can’t access the scope of any functions contained within the module. We’ll explain all of this again in the section on lexical scope in a few moments.
Execution Context
Closely related to scope is the execution context or simply context. Context, in simple terms, refers to the value of the this
keyword, while scope is related to variable resolution and access. We’ll explore this
in detail below.
Unlike scope, the execution context can change at run time. For example, a function may be called at multiple points in your code and each time will have a different execution context and hence a different value of this
, while the scope will be the same each time. We’ll examine
execution context and this
in more detail below.
Global Scope
At the top level is the global scope. Data in global scope is accessible from anywhere in your code, within every module, class, function, and object. Different JavaScript environments such as browsers and Node.js treat global scope slightly differently.
In the browser, global scope is a top-level object called window
. You can add data to the global scope using window.yourVariableName = ...
:
Now we can access window.myInfo
from anywhere in any JavaScript file that is part of our application.
We’ll explore global scope further in the DOM API chapter.
Module Scope
When writing modular JavaScript, as we’ll be doing throughout this book, every file is a module, and every module creates a scope. For example, here is a module called main.js containing some code. The entire file/module is a scope. Any code we add within this module is created in module scope.
We’ll explore module scope further in the Modules Reference chapter.
Block Scope
Blocks (
defined using {}
) create a scope. Here, we define a variable name within a block, and later when we attempt to log it to the console from outside, it’s not available.
As we mentioned earlier, it’s not that common to see a bare block statement like this. More often you will encounter them as part of an if…else statement, for loop, and so on.
Closures (Function Scope)
Function scope is so important that it gets a special name: closures. Closures were even more important in old-school JavaScript before we had module scope and block scope. Both arrow functions and normal functions create a closure, although, as we’ll see below, these two types of function create different execution contexts.
var
, let
, const
, and Scope
Now we can finally explain the difference between
var
and the new let
and const
. In short, var
is function scoped while let
and const
are block-scoped. Care must be taken when using var
with block scope. We will never use var
in this book so we won’t get into this here, but if you are interested, refer to the
MDN let
reference for more details.
Lexical Scope
Scope in JavaScript, like most modern programming languages, is lexical. This means scopes are arranged in a hierarchy with the global scope at the top and the current local scope at the bottom.
If we want to know what scope a piece of data resides in, and hence where in our code we can access it from, we simply need to look at the active line in our text editor. The state of the program when running has no bearing on the scope of data, in contrast to languages with dynamic scope like Lisp.
At each step, while moving through the scope hierarchy, we call the current scope the child scope and the scope directly above the parent scope. We might also refer to these as outer scope and inner scope (outer scope refers to all levels of scope above the current scope).
In the following examples, the highlighted line refers to the current scope.
This variable is at the top level in the module, hence it’s in module scope. The scope above us in the hierarchy is the global scope.
To access the global scope while working with JavaScript modules, we use the global window
object. Here, we add the variable y
to the global scope.
Next, we’ll create a block inside the module.
Again, you’ll more commonly encounter block scope when working with loops, if…else statement, and other such constructs.
Function scope works similarly to block scope.
These are all simple examples, but in a real body of code you will end up with much deeper nesting of scope. Here are a couple of examples. See if you can figure out the parent and local scope in each case.
Hierarchy of Data Access
As we mentioned above, the scopes are arranged in a hierarchy with global scope at the top and the current scope at the bottom. We can access data from a parent scope inside a child scope, but we cannot access data from a child scope in the parent scope.
Here, that means we can access the name
variable, declared in the parent scope (module scope), from within the child scope (block scope).
However, if we reverse that, we cannot access the variable created in the child scope from the outer scope.
So far so good. But what if we create the variable name
in two different scopes?
In this case, the inner/child scope has precedence over the outer scope. However, once we are back in the parent scope, the variable will have it’s original value again.
As you can imagine, this can quickly become confusing, so we avoid using the same name in inner and outer scope. If your text editor or IDE has a linter, it should complain when you do this.
There’s a lot more to scope than we have covered here, as you will discover while working with JavaScript. However, this should be enough to get you started with the examples in this book.
Execution Context and the this
Keyword
The keyword this
in JavaScript refers to the current execution context. Unlike scope, the value of this
can change at runtime depending on how the code is executed.
As JavaScript is executing your code, it maintains a special hidden variable called thisBinding
. We never see this variable, but we can access it using this
. Depending on how the current piece of code is being executed, thisBinding
can have many possible values, including undefined
.
Deeply understanding how this
works in JavaScript will take you some time, and is beyond the scope of this chapter. Instead, we’ll take a look at the value of this
in some of the common cases we’ll encounter through this book.
this
in Global Scope
In global scope, when executing your code in a web browser, this
refers to the window
object. You can see by opening the browser console and typing this === window
.
this
in Module Scope
When we are at the top level in our main.js module, the value of this
is undefined
.
this
and Functions
The value of this
within a function is the most difficult to understand since it depends on what piece of code called the function, and hence the execution context of the current function call. This can change from one call of the function to the next. To make your life simple, use this
sparingly in functions that are not class methods.
Also, there are differences between normal and arrow functions so we’ll cover the latter separately below. This section refers exclusively to “normal” functions.
When used in a function executed from a module, this
is undefined
.
However, when we run the same function in global scope (for example, by pasting the following code into the browser console), this
refers to the global Window
object.
Next, when we create a function as a property of an object, this
binds to the object itself. When we create a function this way we’ll refer to it as a method. Here, in the obj.testThis
method, this
refers to the obj
itself.
this
and Classes
When used inside a class method, this
refers to the class instance. We used this
in our
Cat
class earlier:
First, we store the parameters name
and age
as instance variables in the constructor
as this.name
, this.age
. Later, we can access these variables from other class methods such as .printName
.
This is similar to the behavior of this
in the obj.testThis
method we created above and function created inside classes are also referred to as methods.
this
and Arrow Functions
Arrow functions treat this
differently than normal functions. Specifically, they don’t have a this
at all. When you access this
inside an arrow function, it will be as if the function didn’t exist and you were using this
from the parent scope. This can be a bit tricky to grasp so let’s illustrate it with an example.
Here, we have created an obj
object with a variable obj.z
and two functions, one normal function, and one arrow function.
We will now test the value of this
in each function, first in the browser console and then in main.js.
First, open up the browser console and paste in the above object. Second, type console.log(this)
. Since you are in global scope, this === Window
.
Next, run obj.normalFunction
. As we saw above, within .normalFunction
, this
refers to the obj
itself.
Finally, run obj.arrowFunction
. Once again, you’ll see that this
refers to Window
.
What this all means is that running console.log(this)
within the global scope and within obj.arrowFunction
gives the same result, but within obj.normalFunction
gives a different result.
Next, do it all again in main.js. Notice that in the outer scope and the arrow function, this
is now undefined.
Other Arrow Function Differences
If you open up the
MDN Arrow functions page, you’ll see that arrow functions don’t have bindings to this
, arguments
, super
, or new.target
keywords. However, for our purposes, the only important difference is the lack of binding to this
.
Use Cases for Arrow Functions and Normal Functions
In general, the difference between arrow functions and normal functions doesn’t matter unless you use this
in the function. Usually, you should avoid using this
in functions, with two common exceptions: class methods, and callback functions. For these cases, remember these rules:
- Always use normal functions for class methods
- Always use arrow functions for callback functions
We will cut short our discussion of context and scope here to prevent this chapter from becoming an entire book. As with scope, you’ll develop a better understanding of how this
works as you continue to work with JavaScript.
The MDN this
reference is only one of
many great guides on this subject.
The Spread Operator
The Spread operator is a quality of life operator available in modern JavaScript. In other words, it doesn’t allow us to do anything we couldn’t do in old school JavaScript, it just makes our life easier.
Suppose that we have an array of objects:
Normally, to add them to our scene, we would have to do this:
The spread operator is three dots: ...
and allows us to do the above with a more concise syntax - in other words, it spreads out the array:
Combining Objects with Spread
We can do something similar to combine two objects. A common use case is to overwrite default parameters with our custom parameters:
We can combine any number of objects using spread, and the values to the right will take precedence. This means the default red
will get overwritten by the custom blue
.
Typed Arrays
Arrays in JavaScript can be any length and can hold any kind of data, in contrast to some programming languages, where arrays have a set length and a set data type.
This makes our lives as developers easy. We don’t have to consider how many things will end up in an array when we create it, or what kind of data we’ll store there. The downside is that it’s harder for the JavaScript engine to optimize. As the array grows in size, the engine must keep allocating memory which can result in memory fragmentation (pieces of the array scattered throughout your RAM), making it slow to access the array data.
In many JavaScript applications you’ll deal with arrays that contain at most a few hundred or thousand entries, so that doesn’t matter too much. However, as three.js developers, we often face challenges that are less common in JavaScript development. Perhaps the most common of these is the use of arrays that contain millions of pieces of data representing things like positions in 3D space.
In these cases, the JavaScript engine will do it’s best to optimize the array based on the kind of data it contains. For example, if the array contains only small numbers and no gaps, it will optimize for this case. However, as soon as we add another type of data or leave a gap in the array, that optimization is thrown out (de-optimization occurs). The other issue, as we just mentioned, is memory fragmentation as huge arrays usually end up split into many small pieces scattered through your computer’s memory.
This all sounds very complicated. Why don’t we help out the JavaScript engine by telling it ahead of time:
- Exactly what type of data the array will contain.
- Exactly how many pieces of data the array will contain.
With these, the JavaScript engine can allocate an exact amount of non-fragmented (contiguous) memory for the array and perform many other optimizations.
This is where typed arrays come in.
Type arrays are similar to normal JavaScript arrays, but we must specify the type of data they contain, and we must specify their exact length ahead of time.
There are quite a few kinds of typed array. We’ll look at two of the ones more commonly used in three.js.
Uint8Array
Uint8Array
holds unsigned integer values of 8 bits, which means each entry in the array can hold a value between 0 and 255 inclusive.
This is in contrast to the Int8Array
which can hold signed integer values of 8 bits, which means values between $-128$ and $127$ inclusive.
Float32Array
Float32Array
stores 32-bit floating-point numbers which means values from $1.2\times10^{-38}$ to $3.4\times10^{38}$ inclusive.
Float32Array
is the typed array most commonly used in three.js since 32-bit floating-point numbers are used to represent positions in 3D space, so we’ll take a look at how to create one here. Everything we cover there applies to all typed arrays.
We can create a new Float32Array
from an existing array as long as that array contains valid data. In this case, the length of the typed array is taken from the length of the array.
If you initialize a typed array using invalid data, you’ll get entries with
NaN
(Not a Number).
Rather than pass in an existing array, we can specify the length directly, in which case all the values in the typed array will be set to zero:
Later, we can add and access data using the array’s indices, just as we do with normal arrays.
Differences Between Typed Arrays and Normal Arrays
If you attempt to push new data to the array rather than use the index, you’ll get an error.
Typed arrays lack some of the methods (like .push
) that arrays have. To see the difference, check out the
Typed arrays page on MDN and compare it to
the Array page.
The other important difference is that you cannot increase or decrease the length of a typed array after creating it. If you need to change the length, you have to throw away the old one and create a completely new typed array.
However, aside from these differences, typed arrays behave like normal JavaScript arrays. You can access elements by index and using forEach
and other methods:
If you try to set or access an index greater than the length of a typed array, nothing will happen. This is in contrast to normal arrays where the length of the array will simply be increased.
The Math Object
JavaScript comes with lots of built-in mathematical helper methods and constants, all of which are stored in the global Math
object.
We’ll find many of them useful, for example, Math.PI,
Math.sin
, Math.cos
, Math.tan
, Math.abs
, Math.abs
, Math.sqrt
, and Math.random
. There’s a complete reference on the
MDN Math Object page.
The Math
object is built-in to JavaScript, and there are many other built-in objects, for example, TypedArrays
, Maps
, Sets
, and so on. These are all available in any JavaScript environment such as the browser or Node.js.
However, a lot of the built-in functionality we’ll encounter when using JavaScript comes from the web browser and is unavailable in other environments. We can access this functionality using something called the DOM API. For example,
the global Window
object that we’ve encountered several times throughout this chapter is part of the DOM API, provided by the browser.
The DOM API allows us to interact with an HTML based web page using JavaScript, and we’ll explore the parts of it that we’ll be using in the next chapter.