JavaScript was created in 1995 as a way to add programs to web pages in the Netscape Navigator browser. Today the language has been adopted by all the other major web browsers, and it has become one of the most popular programming languages in the world.
In this tutorial, we are going to explore the fundamentals of JavaScript and discuss how to use it to create web applications, as both the frontend and the backend.
This tutorial is originally published at thedevspace.io.
You can also access the source code for this tutorial here 👈.
Setting up the environment
This tutorial comes with many example code snippets. To execute the them, open up you browser, and go to Developer Tools -> Console:
Or you can install Node.js on your computer, which allows you to run JavaScript programs using command terminals.
You can refer to the linked article for details on how to set up Node.js on your computer.
Data types in JavaScript
In the computer world, it's all about data. What a computer program does is essentially take some input data, process them, and then return some output data. In this section, let's talk about some different types of data that JavaScript can process.
Numbers
Numbers are the easiest because it works exactly like what you studied in your math class in elementary school.
// Integer
100;
// Fractional Number
10.56;
//Scientific Notation
3.14e5; // 3.14 * 10^5 = 314000
The primary usage of numbers is to perform arithmetic operations.
3 + 5 * 2; // -> 13
Just like you studied in elementary school, multiplications and divisions happen first. However, you can change this by using parentheses.
(3 + 5) * 2; // -> 16
There is one operator that you might not recognize, which is the modulo (%
) operation. X % Y
calculates the remainder of dividing X
by Y
. For example:
25 % 5; // -> 0
25 % 10; // -> 5
25 % 15; // -> 10
Strings
Strings are used to represent texts, and they are all enclosed in quotes like this:
"This is a string.";
"This is also a string.";
Both single and double quotes work exactly the same, as long as the opening and the closing quotes match each other.
Whenever a backslash (\
) is found inside a string, it means the character after it has a special meaning. For example, when the backslash is followed by the letter n (\n
), this will be interpreted by your computer as a new line:
"This is the first line\nThis is the second line";
The output text would look like this:
This is the first line
This is the second line
The +
operation can also be used on strings as well. But obviously, strings can not be used in arithmetic operations, the plus sign here means concatenate (connecting two strings together).
"con" + "cat" + "e" + "nate"; // -> "concatenate"
Finally, there is a special kind of string in JavaScript, the backtick-quoted strings, usually called template literals. It allows us to embed other values inside the string:
`half of 100 is ${100 / 2}`;
In this example, the division inside ${}
will be calculated, the result will be converted into a string and printed in that position. So this example will give us:
half of 100 is 50
Boolean values
The Boolean type only includes two values, true
and false
. Comparison is the most common way to produce boolean values.
console.log(1 == 1); // -> true
console.log(1 > 2); // -> false
console.log(1 < 0); // -> false
console.log(1 != 2); // -> true
In this example, ==
means equal, and !=
means not equal. Other similar operators include >=
(greater than or equal to) and <=
(less than or equal to).
There are three logical operators that we can apply to Boolean values in JavaScript, &&
(and), ||
(or), and !
(not).
The &&
operator denotes logical and
, it produces true
only if both values given to it are true
.
console.log(true && false); // -> false
console.log(false && true); // -> false
console.log(false && false); // -> false
console.log(true && true); // -> true
The ||
operator denotes logical or
, it produces true
if either of the values given to it is true
.
console.log(true || false); // -> true
console.log(false || true); // -> true
console.log(false || false); // -> false
console.log(true || true); // -> true
The !
operator denotes logical not
, and it flips the given value.
console.log(!true); // -> false
console.log(!false); // -> true
We can also mix arithmetic operations with comparisons and logical operations.
1 + 1 == 2 && 1 + 1 < 0;
In this example, 1 + 1 == 2
gives us true
, and 1 + 1 < 0
gives us false
, so we have
true && false; // -> false
Empty values
There are two special values in JavaScript, null
and undefined
. They indicate the absence of a meaningful value. In computer programs, there are a lot of operations that do not produce meaningful results (which we will see later in this course), and these results will be denoted by null
or undefined
.
These two values have virtually no difference, in fact, in most cases, you can treat them as interchangeable. The fact that there are two different values indicating the same thing is just an accident of JavaScript's design.
Data type conversion
JavaScript is a very intelligent programming language, it will always try to execute the program you give it, even though the program does not make sense. For example:
console.log(8 * null); // -> 0
console.log("5" - 1); // -> 4
console.log("5" + 1); // -> "51"
In the first example, the null
gets converted into the number 0
, and in the second example, the string "5"
becomes the number 5
. However, in the third example, the number 1
gets converted into the string "1"
, and the plus sign here means concatenate, so the result becomes "51"
.
Yes, these results are all over the place, and they don't make sense at all. This is why you should never try to do this when you are coding, even though it "works", it will lead to unexpected results.
Program structures in JavaScript
Statements and bindings
In computer programming, you can think of a "program" as an instruction manual to solve a complex problem. Each instruction/sentence in that manual is called a statement. In JavaScript, a statement should always end with a semicolon(;
).
let num = 10;
This example is called a binding, or variable. It binds the value 10
to the name num
using the =
operator, which allows us to do something like this:
let num = 10;
console.log(num * num); // -> 100
The keyword let
indicates that this statement is going to define a binding. When a binding is formed, it does not mean that the name is tied to the value forever, we can still use the =
operator on existing bindings.
let num = 10;
console.log(num); // -> 10
num = 20;
console.log(num); // -> 20
Notice that we only used the keyword let
in line 1. That is because let
is only used to declare a binding, and in line 5, we are merely updating the value that is tied to the variable num
.
let num1 = 10;
let num2 = 20;
console.log(num1); // -> 10
console.log(num2); // -> 20
num2 = num1;
console.log(num1); // -> 10
console.log(num2); // -> 10
let num = 10;
num = num - 5;
console.log(num); // -> 5
The keywords const
and var
can also be used to create bindings just like let
, however, they are different in terms of scopes, which we will in detail later.
Functions
A function is a piece of program that returns a value or has some side effects, or both. For example, the console.log()
function we have seen a few times is used to output values in the terminal.
Or, in this example, the prompt()
function will show you a dialog that asks for user input, and that input will be bound with the variable num
.
let num = prompt("Enter A Number");
console.log(num);
Both showing a dialog and writing text to screen are side effects. A function can also be useful without the side effect. For example:
console.log(Math.max(2, 4, 6, 8));
The Math.max()
function does not have any side effects, it simply takes a set of numbers and returns the greatest.
All of these functions are built into JavaScript. We can, however, create our own functions using JavaScript. We will discuss this topic in the next section.
if statements
The if
statement offers us a way to execute different pieces of code under different conditions. For example:
let num = prompt("Enter A Number");
if (num < 10) {
console.log("Small");
} else {
console.log("Large");
}
This program asks you to input a number, if the number is less than 10, console.log("Small");
will be executed, and the program will output "Small"
. If the number is larger than 10, the program will output "Large"
.
We can also chain multiple if
/else
pairs if there are multiple conditions we need to consider:
if (num < 10) {
console.log("Small");
} else if (num < 100) {
console.log("Medium");
} else {
console.log("Large");
}
This program will first check if the number is less than 10, if it is, it will output "Small"
. If the number is greater than 10, the program will then check if it is less than 100. If it is, the program will output "Medium"
. Finally, if the number is greater than 100, the program will show "Large"
.
for loops
The for loops offer us a way to execute the same code over and over again, as long as some conditions are satisfied.
for (let num = 0; num <= 12; num = num + 2) {
console.log(num);
}
A for
loop takes three expressions, separated by two semicolons. In this example, the first expression let num = 0
declares a new variable num
, whose initial value is 0. The second expression means the loop will iterate until the condition num <= 12
is violated (num
is larger than 12). The last expression means for each iteration, num
will add itself by 2.
while loops
while
loops work in a similar way, except it only takes one expression. In fact, we can easily change our previous for
loop example into a while
loop.
let num = 0;
while (num <= 12) {
console.log(num);
num = num + 2;
}
In this example, we initiated the num
variable first, outside of the while
loop. Inside the parentheses, after the keyword while
is the expression that checks whether the loop should continue. Finally, we update the value of num
at the end of the while
loop.
do while loops
A do-while
loop differs from a while
loop only on one point, it guarantees that the body of the loop executes at least once.
let num = 10;
do {
num = num + 1;
console.log(num);
} while (num <= 1);
This time the initial value of num
is 10, which violates the condition for the loop to continue. But because this is a do-while
loop, the body is still executed once. If this was a while
loop, it would not execute at all.
Breaking out of a loop
Violating the condition for the loop to continue is not the only way we can stop a loop. For instance, you are asked to find a number that is greater than 100, and divisible by 9 (Recall that %
operator is used to calculate reminder, so if the remainder of x/9
equals 0, that means x
is divisible by 9.). We can use a for
loop to solve this problem:
for (let num = 100; ; num = num + 1) {
if (num % 9 == 0) {
console.log(num);
break;
}
}
Notice that we do not have an expression that decides whether the loop should continue. Instead, we have an if
statement with a break
keyword inside, which will break out of the loop if it is executed. If you remove the break
keyword, this for
loop becomes an infinite loop and will run forever, which is something you should always avoid.
Functions in JavaScript
Previously, we've seen some functions that come with JavaScript. In this section, we are going to focus on defining our own custom functions in JavaScript. A function can be seen as a piece of code wrapped in a value, which allows us to reuse that piece of code over and over again. In this tutorial, we are going to talk about three different ways we can define a function in JavaScript.
The first method is to define functions as values, and bind that value to a name (like how we defined variables previously).
let square = function (x) {
return x * x;
};
The function is created with the keyword function
, and it will take a set of parameters as input, in this case, only x
. A function should also have a body where you return an output using the keyword return
, or have some kind of side effect. Lastly, the function as a value will be assigned to the name square
, which we need to use to invoke this function.
Also, remember that the semicolon (;
) at the end is necessary because it is still a full statement where you declare a binding, except the value here is a function.
console.log(square(10)); // -> 100
A function can have more than one parameter or no parameters at all (an empty set of parameters).
const sleep = function () {
console.log("zzzzzzzzzzzzzzzzzzzzzz");
};
var multiply3 = function (x, y, z) {
return x * y * z;
};
As you can see, it is possible for a function to have only a side effect and not return anything.
The second method is slightly shorter, by declaring a function using the function
keyword, and it doesn't require a semicolon at the end:
function square(x) {
return x * x;
}
The method also allows us to do something like this:
sleep();
multiply3(2, 3, 4);
function sleep() {
console.log("zzzzzzzzzzzzzzzzzzzzzz");
}
function multiply3(x, y, z) {
return x * y * z;
}
Here we put the function declarations after the statement that calls them, and the code still works. Now, we can put all the functions in one place, which is a good thing for future maintenance.
The third method is called arrow functions. Instead of the keyword function
, we can use an arrow (=>
) to declare a function.
const square = (x) => {
return x * x;
};
This is the exact same square()
function we saw before, and it works exactly the same. Then why does JavaScript have both arrow functions and the function
keyword? While, in some cases, it allows us to write shorter functions.
If the function only has one parameter, then you can omit the parentheses around the parameter list. And if there is only one statement in the function body, the curly braces and the return
keyword can also be omitted. Then, our square()
function becomes:
const square = (x) => x * x;
Bindings and scopes
Before we go deeper into the topic of functions, let's go back to the first method. You may have noticed that we defined the functions in the examples using different keywords, let
, const
and var
. What exactly are their differences?
First, we need to understand the concept of scope. It is the part of the program in which the binding is accessible. If a binding is defined outside of any functions or blocks (blocks can be if
statements, for
or while
loops, etc.), then you can refer to that binding wherever you want. This is called a global binding.
If the binding is declared inside a function or block using let
or const
, that binding will only be accessible from inside the function/block, and that is called a local binding. However, if the binding is defined using the keyword var
, then that binding will also be accessible from outside of the function/block.
let x = 10;
if (true) {
let y = 20;
var z = 30;
console.log(x + y + z); // -> all three variables are accessible here
}
console.log(x + z); // -> you cannot "see" y from here, but z is still accessible
Now, what are the differences between let
and const
? As the name suggests, const
stands for constant, meaning once a binding is declared using const
, you cannot change its value (unlike let
).
Optional arguments
JavaScript is very broad-minded when it comes to the number of parameters you pass to the function. For example, we have the square()
function we defined before, which is supposed to take one argument.
function square(x) {
return x * x;
}
console.log(square(4, true, "qwerty"));
In this example, we gave the square()
function more than one argument, and it simply ignores the extra arguments and computes the square of the first one.
And if we passed too few arguments, those missing parameters will be assigned the value undefined
instead of giving you an error.
The downside of this is, of course, when you accidentally make a mistake, no one will tell you about it. So, even though it technically works, you should never rely on this, it could give you some unexpected results. Instead, you should always be careful how many parameters you need, and how many arguments you are passing to the function.
Rest parameters
However, what if you don't know how many parameters you need? For example, you are designing a function that finds the maximum number in a series of numbers, but you don't know how many numbers are in the series, so you need to design a function that takes any number of arguments.
To write a function like this, you need to put three dots before the function's last parameter:
function max(...numbers) {
let result = -Infinity;
for (let number of numbers) {
if (number > result) {
result = number;
}
}
return result;
}
max(1, 2, 3, 4, 5, 6, 7);
Now, the parameter numbers
(it is called the rest parameter) will be bound to an array, and the function will return the maximum number in that array.
An array is a list of items, in this case, we have [ 1, 2, 3, 4, 5, 6, 7 ]
, and for (let number of numbers)
is how we can iterate over all items in this array. We'll discuss arrays in JavaScript later.
Recursions in JavaScript
Finally, let's talk about the concept of recursion. Recursion is when a function calls itself. The most typical example is how we calculate the power of a number.
function power(base, exponent) {
if (exponent == 0) {
return 1;
} else {
return base * power(base, exponent - 1);
}
}
Notice that in line 5, the function power()
called itself with parameters base
and exponent - 1
. This is a bit confusing, but don't worry, to understand this code, let's plug in some numbers. Let's try to calculate 10^5
(10 to the power of 5).
In the first step, we simply plug in the numbers, and the function returns 10 * power(10, 4)
. Then we need to calculate power(10, 4)
. Plug in the numbers, and we get 10 * power(10, 3)
, which means power(10, 5)
equals 10 * 10 * power(10, 3)
.
And we keep repeating the same steps until we get 10 * 10 * 10 * 10 * 10 * power(10, 0)
. Because power(10, 0)
returns 1
, eventually we get power(10, 5)
equals 10 * 10 * 10 * 10 * 10
.
This is a very elegant way of defining exponentiation, but unfortunately, this method is about three times slower than using loops in JavaScript. This is a dilemma that programmers face all the time, we have to choose between simplicity and speed because almost any program can be made faster by making it bigger. It's up to the programmer to decide on an appropriate balance.
Creating arrays in JavaScript
Previously, we discussed some data types that are built into JavaScript, such as strings, numbers, and Boolean values. In this section, we are going to talk about two new data types that allow us to group all of these values together to create more complex structures, they are called arrays and objects.
Let's first talk about arrays. An array is a list of values wrapped inside a pair of square brackets, separated by commas.
let listOfNumbers = [1, 2, 3, 4, 5];
let listOfStrings = ["1", "2", "3", "4", "5"];
We can access the elements in the array by referring to their index number, which starts from 0 instead of 1. This is a very common thing in computer programming, you should get used to it.
let x = listOfNumbers[0]; // x = 1 (index 0 is the first element)
let y = listOfNumbers[2]; // y = 3 (index 2 is the third element)
What if we want to access the last element of the array, and we don't know the length of the array? Instead of trying every index starting from 0 to see if it gives a value, we can access the length of the array using arrayName.length
. The length
here is called a property and .
is how we can access the property of a value. For example:
let z = listOfNumbers[listOfNumbers.length - 1]; // z = 5
In this example, listOfNumbers.length
gives us 5, because we start from 1 when we calculate length. However, since the index always starts from 0, that means the index of the last element should be 1 less than the length, hence the listOfNumbers.length - 1
here. We'll discuss properties and methods (properties with function values) in detail later.
It is also possible to change the values in the array.
let listOfNumbers = [1, 2, 3, 4, 5];
listOfNumbers[2] = 100;
// listOfNumbers = [1, 2, 100, 4, 5];
Array loops
In some cases, we may need to iterate over the entire array and access each element one by one. There are two different ways we can do this in JavaScript.
let list = [...];
for (let e of list) {
...
// Do something with the variable e
}
This is the method we've seen before, for every iteration, the variable e
will be assigned the next element in the array list
, and you can do something with the variable e
inside that for
loop.
The second method is to use the index.
for (let i = 0; i < list.length; i++) {
let e = list[i];
}
In this example, the variable i
is bound to the index of the array elements, and e
is bound to the array element with the index i
. The expression i++
is a shorthand notation of i = i + 1
.
Stacks and queues
Stacks and queues are two very important data structures in computer programming, which we can implement using arrays.
A stack is a structure of elements based on the principle of last in first out (LIFO). It is like a stack of books. If you want to add a new book to the stack, you would put it on the top, and if you want to remove a book, you would remove the one on the top as well.
The stack data structure has two fundamental operations:
The push operation is used to insert a new element onto the stack.
The pop operation is used to remove the most recent element from the stack and return its value.
Luckily, JavaScript offers these two methods out of the package. To use them we can, again, use the .
operator, because methods are just properties with function values:
let stack = [];
stack.push(2);
stack.push(5);
stack.push(7);
stack.push(9);
stack.push(7);
console.log(stack); // -> [2, 5, 7, 9, 7]
stack.pop(); // -> returns 7
stack.pop(); // -> returns 9
stack.pop(); // -> returns 7
stack.pop(); // -> returns 5
stack.pop(); // -> returns 2
console.log(stack); // -> []
A queue is another very useful data structure. It is very similar to stacks, except it follows the first in first out (FIFO) principle. It's like waiting in line in a restaurant, if you come first, you will get the food first.
The queue data structure has two fundamental operations as well:
The enqueue operation is used to insert a new element at the end of the queue.
The dequeue operation is used to remove the element at the beginning of the queue and return its value.
There are also two methods built into JavaScript that help us with these two operations, however, the terminologies are a bit different. For the enqueue operation, we can use the push()
method, because it pushes the new element to the end of the array. As for the dequeue operation, we can use the shift()
method, which removes the first element of the array.
let queue = [];
queue.push(2);
queue.push(5);
queue.push(7);
queue.push(9);
queue.push(7);
console.log(queue);
queue.shift();
queue.shift();
queue.shift();
queue.shift();
queue.shift();
Objects in JavaScript
Properties and methods
Now, let's take a closer look at the concept of property which we were just talking about. We've seen some strange-looking expressions like listOfNumbers.length
and Math.max
. These are expressions that access the property of some value. In the first example, we access the length
property of the listOfNumbers
array. In the second example, we access the max
property in the Math
object.
Almost all of the data types we talked about have built-in properties. For example, a string also has a length
property just like arrays, which store the length of that string.
In addition to the length
property that only holds a number, there are also a number of properties that hold function values. For instance, we could use the toUpperCase
property of a string to get a copy of that string in which all letters in that string are converted to uppercase. We usually refer to these properties with function values as methods.
let string = "abCDefg";
console.log(string.toUpperCase()); // -> "ABCDEFG"
console.log(string); // -> "abCDefg"
Notice that invoking the toUpperCase()
method does not change the string
variable's original value.
Write our own properties
All of the properties we've talked about so far are built-in properties, and they all come with JavaScript. But, what if we want to create our own properties? Objects are the second data type we are going to discuss here which allows us to create our own properties.
An object is an arbitrary collection of properties, defined using the curly braces {}
. For example, here we define an object named house
:
let house = {
members: 4,
names: ["Mason", "Robert", "Lara", "Wynne"],
};
Inside the braces is a list of properties separated by commas. Each property is defined in name: value
format.
In this example, there are four members in the house. To access that information, we can use the same notation we used before, with a dot (.
).
console.log(house.members); // -> 4
The objects are also mutable, which means their values can be modified. We can do that by using the =
operator. For example:
house.members = 5;
console.log(house.members); // -> 5
However, this only applies to the properties we created ourselves. The properties of the other data types, such as strings, numbers, and arrays, their properties are immutable, and cannot be modified. For instance, if you have a string that contains "cat"
, it is not possible for you to write some code to change a character in that string to make it spell "rat"
.
JSON
Before we move on, we are going to introduce another data structure that is widely used in the web dev field, called JSON.
When you define a property (name: value
), the name
does not actually contain its value
. Instead, it is stored in the computer memory as an address, which points to the location in the memory where the value
is stored.
If you want to save the data in a file or send it to someone else over the internet, you'll have to somehow convert these tangles of memory address into a description that can be stored or sent over the internet. This process is called serialization, which means the data is converted into a flat description. A popular serialization format is called JSON(JavaScript Object Notation, pronounced "Jason").
JSON looks just JavaScript's way of defining objects, with a few extra restrictions. The property names have to be surrounded by double quotes, and no functions or anything that involves actual computation, only the simple data types are allowed. So, if we express our house
object in JSON format, it would look like this:
{
"members": 4,
"names": ["Mason", "Robert", "Lara", "Wynne"]
}
JSON is widely used as a data storage and communication format on the web, even in languages other than JavaScript. We will encounter it again as we talk about backend development in the future.
What is object-oriented programming
In the previous section, we talked about a new data type called the objects. In computer programming, objects are not just a simple data structure, it is very commonly used as a way to organize code. Programmers would group values and functions with close relationships to each other, and put them in the same object, which makes them easier to access. This method of organizing your code is called object-oriented programming. In this section, we'll discuss how these ideas could be applied in JavaScript.
Encapsulation
The core idea of object-oriented programming is to split a program into small pieces, and each piece only minds its own business. People working on other pieces of code don't need to know how this piece of code is written, or that it even exists.
Sometimes the different pieces need to communicate with each other to perform a more complicated task. To make this work, programmers would create a property/method inside the object that is allowed to talk to the outside, this method is said to be made public, and they are usually referred to as the interface. While the actual implementation is hidden inside the object as private properties, meaning they cannot be seen or accessed by the outside code. This way of separating the interface from the implementation is called encapsulation.
Most programming languages have very distinctive methods of denoting public and private properties, usually with keywords public
and private
. JavaScript, however, does not have this functionality built-in, at least not yet. But JavaScript programmers still follow this idea of encapsulation, by putting an underscore character (_
) at the beginning of the properties that should be made private. But since this is not JavaScript's built-in functionality, technically you could still access these properties from the outside, but that is something you should never do, for security reasons.
Methods
As you know, methods are just properties with functions as their values. This is a simple method:
// Create a new empty object
let rabbit = {};
// Add a method named speak() to the empty object
rabbit.speak = function (line) {
console.log(`The rabbit says '${line}'`);
};
// Excute the mathod
rabbit.speak("I'm alive.");
Sometimes, the method needs to do something to the object it was called on, such as taking two numbers that are stored in the object, and adding them up, or taking a string value from the object and processing it. To do this, we can use the this
keyword, which is a binding that automatically points to the object that was called on. Let's take a look at an example:
// Create the method named speak()
function speak(line) {
console.log(`The ${this.type} rabbit says '${line}'`);
}
/*
Create an object named whiteRabbit, with two properties, "type"
and "speak". By using the "this" keyword in the method "speak",
we are able to access the "type" property in the same object.
*/
// In this case, this.type = "white".
let whiteRabbit = { type: "white", speak };
// In this case, this.type = "hungry".
let hungryRabbit = { type: "hungry", speak };
Prototypes
Look at the following code:
// Create an empty object
let empty = {};
console.log(empty.toString); // -> function toString(){...}
console.log(empty.toString); // -> [object Object]
Notice that even though we defined an empty object, we still manage to pull a property from it. Well, technically, that property is not from the object, it's from the object's prototype. A prototype is basically another object on which our empty
object is based, and it acts as a fallback source of properties. If you are trying to access a property that does not exist in the object, JavaScript will automatically search its prototype for that property.
JavaScript offers a method (Object.getPrototypeOf()
) that returns the prototype of a data type. For example, let's try finding out the prototype of that empty object we just created:
console.log(Object.getPrototypeOf(empty)); // -> {..., constructor: Object(), ...}
console.log(Object.getPrototypeOf(empty) == Object.prototype); // -> true
The Object.prototype
is the ancestral root of all objects that we create, but not all data types share the same prototype. For instance, the functions derive from Function.prototype
, and arrays derive from Array.prototype
.
console.log(Object.getPrototypeOf([]) == Array.prototype); // -> true
console.log(Object.getPrototypeOf(Math.max) == Function.prototype); // -> true
However, since those prototypes are still just objects, they also have a prototype, and that is usually Object.project
. This is why almost all of the data types we've talked about have a toString method that converts objects into a string representation.
In fact, we can create our own prototype and use Object.create()
method to create objects using a specific prototype.
// Create an object, which we'll use as a prototype
let protoRabbit = {
speak(line) {
console.log(`The ${this.type} rabbit says '${line}'`);
},
};
// Create a new object using the protoRabbit as the prototype
let killerRabbit = Object.create(protoRabbit);
killerRabbit.type = "killer";
// Try to access the speak() method from the killerRabbit object
killerRabbit.speak("SKREEEE!"); // -> The killer rabbit says 'SKREEE!'
Classes
In object-oriented programming, there is a concept called class, which works just like the prototypes. A class defines the shape of a type of object (just like prototypes), what kind of properties and methods it has. Such an object is called an instance of the class.
To create an instance of the class, we need to make a new object, which derives from the prototype/class. But you also have to make sure that the object has the properties that an instance of the class is supposed to have, not just the ones derived from the prototype/class. This is what a constructor function does.
// An example of a constructor function
function makeRabbit(type) {
// Create a new object using protoRabbit as prototype
let rabbit = Object.create(protoRabbit);
// Add a property named "type".
// Note that the senond "type" is the variable that is passed to the function
rabbit.type = type;
// returns the newly created object
return rabbit;
}
If you are familiar with other programming languages that follow the idea of object-oriented programming, you'll see that this is a very awkward way of defining a class and constructor function, but it does help you understand what a constructor function is. Luckily, after 2015, JavaScript offered us a new and more standard way of making a class, by using the keyword class
.
let Rabbit = class Rabbit {
constructor(type) {
this.type = type;
}
speak(line) {
console.log(`The ${this.type} rabbit says '${line}'`);
}
};
To create an instance of this class, we can use the keyword new
.
let killerRabbit = new Rabbit("killer");
let blackRabbit = new Rabbit("black");
The constructor()
function which we defined in the class will be automatically executed when you run this code.
Getters, setters, and statics
Now, let's focus on the interface part of object-oriented programming. In case you forgot, the interface is the part of the object that can be "seen" from the outside. Programmers use the interface to make different pieces of code work together to solve a complex problem.
There are typically two types of these interface methods, getters and setters. Getters retrieves information from the object, and setters write information to the object. Let's consider this example of a temperature converter.
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
get fahrenheit() {
return this.celsius * 1.8 + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) / 1.8;
}
static fromFahrenheit(value) {
return new Temperature((value - 32) / 1.8);
}
}
let temp = new Temperature(22);
Notice that we have a static
method in this example. Statics are not part of the interface, they are in charge of attaching additional properties to your constructor function, instead of the prototype. In our example, it is used to provide a different way of creating a class instance, using Fahrenheit.
Inheritance
JavaScript also provides us with an easy way to create a class based on another class, with new definitions of some of its properties. For example, the following class defines a matrix. In case you don't know, a matrix is a two-dimensional array.
class Matrix {
constructor(width, height, element = (x, y) => undefined) {
this.width = width;
this.height = height;
this.content = [];
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
this.content[y * width + x] = element(x, y);
}
}
}
get(x, y) {
return this.content[y * this.width + x];
}
set(x, y, value) {
this.content[y * this.width + x] = value;
}
}
There is another type of matrix that is called a symmetric matrix. It has all the characteristics of a regular matrix, except it is symmetric along its diagonal. To create such a matrix and avoid rewriting the same code all over again, we can make the SymmetricMatrix
extends the Matrix
class like this:
class SymmetricMatrix extends Matrix {
constructor(size, element = (x, y) => undefined) {
super(size, size, (x, y) => {
if (x < y) return element(y, x);
else return element(x, y);
});
}
set(x, y, value) {
super.set(x, y, value);
if (x != y) {
super.set(y, x, value);
}
}
}
let matrix = new SymmetricMatrix(5, (x, y) => `${x},${y}`);
console.log(matrix.get(2, 3)); // → 3,2
Exploring the DOM structure in JavaScript
Starting from this point, we are going to dive into the practical application of the JavaScript language in web development. We'll talk about how JavaScript, HTML, and CSS can work together to make your web pages more appealing and interactive.
Let's start with a quick review of the document object model. Here is a simple HTML document:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Example Page</title>
</head>
<body>
<h1>Example Page</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem ipsum dolor
sit amet, consectetur adipiscing elit.
</p>
<div>
<p>
Vestibulum fringilla lorem ac elit suscipit, nec suscipit nunc posuere.
</p>
<p>
Proin efficitur eros scelerisque consequat
<a href="https://www.ericsdevblog.com/">pulvinar</a>.
</p>
</div>
</body>
</html>
Each HTML element can be seen as a box. For example, the example document has the following structure:
For each box, JavaScript automatically creates a corresponding object, which we can interact with to find out more details about that box, such as its content, attributes, etc. This kind of representation is referred to as the document object model, or DOM for short.
The second important feature of this DOM structure is that the boxes are all connected to each other, which means if we pick a starting point, it is possible for us to move to any other node on the page. For example, the <body>
node has three child elements, <h1>
, <p>
and <div>
. The <div>
node has another two paragraphs (<p>
) has child elements. So, to locate the paragraph with a link (<a>
) in this example document, we can go from <html>
to <body>
to <div>
and finally, locate the <p>
node.
JavaScript and HTML
To import JavaScript code into an HTML document, we can use the <script> </script>
tag.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Example Page</title>
</head>
<body>
...
<!--JavaScript-->
<script>
...
</script>
<!--JavaScript in External File-->
<script src="myScript.js"></script>
</body>
</html>
It is customary to put the JavaScript code before the end of the <body>
tag. There are two ways to insert JavaScript, just like CSS, you can put it together with HTML, or you can put JavaScript in a separate file. For this tutorial, to make things easier, we'll put HTML and JavaScript code together.
JavaScript treats each DOM box as an object, and that allows us to access any element in the HTML document using the global binding document
. For example, document.body
refers to the <body>
element of the document.
...
<body>
...
<!--JavaScript-->
<script>
// Access body element
let body_element = document.body;
// Access h1 element
let h1_element = document.body.firstElementChild;
console.log(h1_element.tagName);
// Access paragraph element (with link)
let p_element = document.body.childNodes[5].lastElementChild;
console.log(p_element.tagName);
</script>
</body>
Go to Developer Tools -> Console in your browser, and you should see that the correct tag names have been returned.
Notice that the index number we use to locate the <div>
element is 5, that is because the childNodes()
method will return not only element nodes but also text nodes and comment nodes. For example, a paragraph element would have an element node <p>
, and a text node, which is its content.
In web development, it is possible to reach any specific element in the document by starting at document.body
and following a fixed path of properties. However, even though that's possible, it's still a bad idea, especially when you have a big HTML document with a complicated relationship tree. It is very easy to make a mistake. Luckily, JavaScript offers us some smarter ways of locating elements in an HTML document.
Locating HTML elements using JavaScript
We mentioned before that JavaScript treats all HTML elements as objects, which implies there are built-in methods for us to use. In fact, there are several different ways we can locate elements in an HTML file using JavaScript, and they actually work a lot like the selectors we talked about in our CSS course.
For instance, all HTML elements have a getElementsByTagName()
method, which helps us locate elements with a specific tag.
<body>
<h1>Example Page</h1>
<p class="paragraphs">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem ipsum dolor
sit amet, consectetur adipiscing elit.
</p>
<div>
<p class="paragraphs paragraphs_div">
Vestibulum fringilla lorem ac elit suscipit, nec suscipit nunc posuere.
</p>
<p class="paragraphs paragraphs_div" id="paragraph_link">
Proin efficitur eros scelerisque consequat
<a href="https://www.ericsdevblog.com/">pulvinar</a>.
</p>
</div>
<!--JavaScript-->
<script>
// Get all paragraph elements
let p_elements = document.body.getElementsByTagName("p");
</script>
</body>
This method will return a collection of elements with the specified tag, and you can access each one of them by specifying the index number, just like an array.
<!--JavaScript-->
<script>
// Get all paragraph elements
let p_elements = document.body.getElementsByTagName("p");
// Get the second paragraph element and print its content
let second_p = p_elements[1];
console.log(second_p.innerHTML);
</script>
However, do not confuse this collection with an actual array, they are very similar, but not entirely the same. We cannot loop over it using for
/of
loop, we have to use the index numbers and run over the elements using a regular for
loop. Or we can transform this collection into an array using Array.from
method.
Once we've found the element we are looking for, we can access the attribute and content of that element using the dot (.
) operator, and we can change their values as well:
<!--JavaScript-->
<script>
// Get all paragraph elements
let p_elements = document.body.getElementsByTagName("p");
// Get the second <p> element
let second_p = p_elements[1];
// Print its content
console.log(second_p.innerHTML);
// Change its content
second_p.innerHTML = "Changed content.";
// Print its attributes
console.log(second_p.attributes);
// Access one of the attributes
console.log(second_p.getAttribute("class"));
</script>
The second method is document.getElementById()
, it is used to find one single element, instead of returning a collection of elements. Note that this method does not exist under every element object, there is no document.body.getElementById()
.
<!--JavaScript-->
<script>
// Get an element based on ID
let element = document.getElementById("paragraphLink");
console.log(element.innerHTML);
</script>
The third method is similar, it helps us locate elements with the same class name. That is getElementsByClassName()
, which searches the entire document to find a collection of elements with the specified class name.
<!--JavaScript-->
<script>
// Get an element based on class name
let element = document.getElementByClass("text-red");
console.log(element.innerHTML);
</script>
The last method is the combination of all of them, which is why it is the most commonly used method when we are trying to locate an element in HTML, it is the querySelector()
.
document.querySelector("div"); // Matches a tag
document.querySelector("#myID"); // Matches an ID
document.querySelector(".myclass"); // Matches a class
Adding and deleting elements
Next, it's time to talk about how to manipulate these HTML elements once we've located them. In fact, almost everything in the DOM structure can be changed.
For instance, we can remove an element like this:
// Get an element based on ID, and then remove it from the page
let element = document.getElementById("paragraphLink");
element.remove();
Or we can create a new element, and add it to the DOM structure:
// Create new paragraph element
let new_p = document.createElement("p");
// Create content for the new <p> element
let new_text = document.createTextNode("This is a new paragraph.");
// Append the content to the <p> element node
new_p.appendChild(new_text);
// Add the new paragraph to the DOM structure
let element = document.getElementById("paragraphLink");
element.append(new_p);
As we mentioned before, a paragraph element should have a <p>
element node, followed by a text node representing its content.
We can also replace one element with another:
// Replace a paragraph with the new paragraph
let element = document.getElementById("paragraph_link");
element.replaceWith(new_p);
In this SECTION, we briefly talked about how to locate and manipulate HTML elements using JavaScript. However, you may have noticed that all the changes are made instantly when we refresh our browser, which is not very interactive. In the next section, we are going to discuss what other events we can use to trigger JavaScript to perform an action.
Event handlers
In computer programming, an event is a user input, such as mouse and keyboard actions, and the program is usually expected to respond with something. This process is called event handling. Let's first take a look at a very simple example. We have an HTML document with a paragraph, and we want the page to return a message when it is clicked.
<p>Click this document to activate the handler.</p>
<script>
// Recall that the () => {} syntax is how we define an arrow function in JavaScript
window.addEventListener("click", () => {
console.log("You knocked?");
});
</script>
This time, the output message will only appear in the console when you click on the document, instead of the moment the page is loaded.
Registering event handlers
The addEventListener()
method is how we can register an event handler for the document node. In fact, we can use the same method to register event handlers for any node in the HTML document. For example:
<!--This time we register an event handler for the button but not the paragraph-->
<button>Click me</button>
<p>No handler here.</p>
<script>
let button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("Button clicked.");
});
</script>
Actually, there is an onclick
attribute for the HTML nodes which will have the exact same effect. However, you can only register one handler for each node that way. By using the addEventListener()
method, we are able to register multiple handlers for each node.
<button>Click me</button>
<script>
let button = document.querySelector("button");
// When you click the button, the console outputs "Button clicked."
button.addEventListener("click", () => {
console.log("Button clicked.");
});
// When you click the button, the console outputs "Button double clicked."
button.addEventListener("dblclick", () => {
console.log("Button double clicked.");
});
</script>
The removeEventListener()
method, call with similar arguments can be used to remove an already registered event handler.
<button>Act-once button</button>
<script>
let button = document.querySelector("button");
function once() {
console.log("Done.");
button.removeEventListener("click", once);
}
button.addEventListener("click", once);
</script>
This button will only work once, after the removeEventListener("click", once)
method is executed, the event handler registered for the button will be removed. The function that is passed to the removeEventListener
has to be the same one that you passed to the addEventListener
method.
Propagation
For most event types, the event handler registered for the node with children can receive events that happened in the children. For example, if a button inside a paragraph is clicked, the event handler registered for the paragraph will also be able to see that click event.
The event is said to propagate outward. For example, if both the button and the paragraph have an event handler, then the handler registered for the button will go first, then the paragraph, and it will keep propagating outward until it reaches the root of the document.
This feature can be quite useful sometimes, however, it is not always what we want. Luckily, we can stop the propagation using the stopPropagation()
method.
<!--<button> is the child of <p>-->
<p>A paragraph with a <button>button</button>.</p>
<script>
let para = document.querySelector("p");
let button = document.querySelector("button");
para.addEventListener("mousedown", () => {
console.log("Handler for paragraph.");
});
button.addEventListener("mousedown", (event) => {
console.log("Handler for button.");
// If the button is clicked with the right mouse button, there will be no propagation
if (event.button == 2) event.stopPropagation();
});
</script>
Sometimes we want to register event handlers for multiple elements on the page. To do this we can use the target
attribute to cast a wide net for a type of event.
<button>A</button>
<button>B</button>
<button>C</button>
<script>
document.body.addEventListener("click", (event) => {
if (event.target.nodeName == "BUTTON") {
console.log("Clicked", event.target.textContent);
}
});
</script>
Default actions
A lot of the events have a default action, for example, when you click on a link, you will be taken to the link's target, if you press the down arrow, the browser will scroll the page down. You can prevent that default action from being activated by using the preventDefault()
method. Let's try something completely useless but very interesting.
<a href="https://developer.mozilla.org/">MDN</a>
<script>
let link = document.querySelector("a");
// When you click the link, instead of going to the URL that link specifies, the console will just output "Nope."
link.addEventListener("click", (event) => {
console.log("Nope.");
event.preventDefault();
});
</script>
Even though this is possible, don't do this unless you have a very good reason to, or it will be very confusing for the users.
Different types of events
Now we have discussed how event handlers work in general, it's time to take a closer look at all the different types of events.
Key events
The first one we are going to talk about is the key event.
When a key on your keyboard is pressed, it will trigger a keydown
event, and when it is released, it triggers a keyup
event.
<p>This page turns violet when you hold the V key.</p>
<script>
window.addEventListener("keydown", (event) => {
if (event.key == "v") {
document.body.style.background = "violet";
}
});
window.addEventListener("keyup", (event) => {
if (event.key == "v") {
document.body.style.background = "";
}
});
</script>
Looks very simple, however, you do need to be very careful about the keydown
event. It is not a one-time thing, instead, it will keep being triggered over and over again, for as long as the key is being pressed, until it is released. You can experiment with the previous code, and see what happens when you keep the key pressed.
There are also some special keys like CTRL
, ALT
, and SHIFT
. These are called modifier keys, they modify the original value of other keys by forming a key combination. For instance, when you press a key while holding the SHIFT
key, "s"
will become "S"
, "1"
will become "!"
etc. We can register event handlers for key combinations like this:
<p>Press Control-Space to continue.</p>
<script>
window.addEventListener("keydown", (event) => {
if (event.key == " " && event.ctrlKey) {
console.log("Continuing!");
}
});
</script>
Pointer events
Pointer, as the name suggests, is used to point at things on the screen. There are primarily two ways that you can use to do that, either with a mouse or a touch screen, and they produce different types of events.
Mouse clicks
Mouse clicks work similarly to key events. When you press a mouse button, a mousedown
event is triggered, and when you release that button, a mouseup
event is triggered. And after the mouseup
event, a complete click is finished, so a click
event will be fired.
<button>Click me!</button>
<script>
let button = document.querySelector("button");
button.addEventListener("mousedown", (event) => {
console.log("mouse down");
});
button.addEventListener("mouseup", (event) => {
console.log("mouse up");
});
button.addEventListener("click", (event) => {
console.log("button clicked");
});
</script>
When two clicks happen very close together, a dblclick
(double click) event will be triggered after the second click.
<button>Double click me!</button>
<script>
let button = document.querySelector("button");
button.addEventListener("dblclick", (event) => {
console.log("double clicked");
});
</script>
Mouse motion
When a mouse pointer moves, a mousemove
event is triggered.
<p>Move the cursor onto this paragraph to turn it red.</p>
<script>
let para = document.querySelector("p");
para.addEventListener("mousemove", (event) => {
para.style.color = "red";
});
</script>
This can be very useful when you are trying to implement some sort of drag and drop functionality. But to do that, we need to first track the location of the cursor. To get that information, we can either use the event's clientX
and clientY
properties, which contain the event’s coordinates (in pixels) relative to the top-left corner of the window, or pageX
and pageY
, which are relative to the top-left corner of the whole document.
For example, the following script will output the coordinates of the click events that happened on the page.
<p>click anywhere</p>
<script>
window.addEventListener("click", (event) => {
console.log("X: " + event.clientX);
console.log("Y: " + event.clientY);
});
</script>
Here is a more complicated example, this program will display a bar, and you can drag it to change its width.
<p>Drag the bar to change its width:</p>
<div style="background: orange; width: 60px; height: 20px"></div>
<script>
let lastX; // Tracks the last observed mouse X position
let bar = document.querySelector("div");
bar.addEventListener("mousedown", (event) => {
if (event.button == 0) {
// if the left button is being held
lastX = event.clientX;
// If the cursor moves while the left button is being held
window.addEventListener("mousemove", moved);
event.preventDefault(); // Prevent selection
}
});
function moved(event) {
// If no button is being held, remove the "mousemove" event handler
if (event.buttons == 0) {
// Notice this is "buttons" not "button"
window.removeEventListener("mousemove", moved);
} else {
let dist = event.clientX - lastX;
let newWidth = Math.max(10, bar.offsetWidth + dist);
bar.style.width = newWidth + "px";
lastX = event.clientX;
}
}
</script>
Notice that we used two different ways to access which button is pushed (The button
property and the buttons
property), and they clearly work differently. Their main difference is that the button
property can only tell you which button (singular) is clicked, while the buttons
property can tell you if a combination of buttons is pushed.
The button
property:
-
0
: Primary button pressed, usually the left button or the un-initialized state -
1
: Auxiliary button pressed, usually the wheel button or the middle button (if present) -
2
: Secondary button pressed, usually the right button -
3
: Fourth button, typically the Browser Back button -
4
: Fifth button, typically the Browser Forward button
The buttons
property:
-
0
: No button or un-initialized -
1
: Primary button (usually the left button) -
2
: Secondary button (usually the right button) -
4
: Auxiliary button (usually the mouse wheel button or middle button) -
8
: 4th button (typically the "Browser Back" button) -
16
: 5th button (typically the "Browser Forward" button)
The buttons property is able to record button combinations. When more than one button is pressed simultaneously, the values are combined. For example, when the primary and secondary buttons are pressed at the same time, the value will be 3
.
Touch events
In most cases, the mouse events will also work when the user is using a touch screen. For example, when you are tapping a button on your screen, it will trigger a click
event, it will be the same as clicking it with a mouse pointer.
However, this won't work in some cases, such as the resizing bar example we talked about before. Because the touch screen doesn't have multiple buttons, and it can't track your finger's position when you are not touching the screen. So to solve this problem, we have a few specific event types triggered only by touch interaction.
When your finger touches the screen, it triggers a touchstart
event, when it moves while touching, it triggers a touchmove
event, and finally, when you lift your finger, it triggers a touchend
event.
Scroll events
A scroll
event is triggered when you place the cursor on an element and scroll the middle button of your mouse. This can be very useful when you are trying to make your webpage more responsive. For example, when you go to the product showcasing page on Apple's website, notice that the elements on the page will move as you scroll down.
Here is an example of a progress bar, it starts at 0% and will go to 100% as you scroll down.
<style>
#progress {
border-bottom: 20px solid orange;
width: 0;
position: fixed;
top: 0;
left: 0;
}
</style>
<div id="progress"></div>
<script>
// Create some content
document.body.appendChild(
document.createTextNode("supercalifragilisticexpialidocious ".repeat(1000))
);
let bar = document.querySelector("#progress");
window.addEventListener("scroll", () => {
let max = document.body.scrollHeight - innerHeight;
bar.style.width = `${(pageYOffset / max) * 100}%`;
});
</script>
Focus events
When an element gains focus, a focus
event will be triggered, and when the element loses focus, a blur
event will be triggered. Unlike the other event types we've discussed, these two do not propagate.
This is most commonly used on HTML field elements. When you click on a text field and start typing some texts, that field is said to be in focus, and when you move on from that field and click on other elements, that field element loses focus.
This is an example that displays help texts for the text field that is currently in focus.
<p>Name: <input type="text" data-help="Your full name" /></p>
<p>Age: <input type="text" data-help="Your age in years" /></p>
<p id="help"></p>
<script>
let help = document.querySelector("#help");
let fields = document.querySelectorAll("input");
for (let field of Array.from(fields)) {
field.addEventListener("focus", (event) => {
let text = event.target.getAttribute("data-help");
help.textContent = text;
});
field.addEventListener("blur", (event) => {
help.textContent = "";
});
}
</script>
Load events
The load
event is triggered when the entire page finishes loading. This is different from directly putting the code inside the <script>
tag directly without event handlers. The code inside the <script>
tag is run immediately when the tag is encountered. This might be too soon in some cases.
There is also a similar event type called beforeunload
. It is triggered when you close a page, the primary use of this event is to prevent the user from accidentally closing their unsaved work.
Regular expression
Regular expression is technically not a part of JavaScript, it's a separate language that is built into JavaScript as well as other programming languages. Regular expression has a very awkward and cryptic syntax, but it is also very useful. It is widely used among programmers as a tool to describe, match and replace patterns in string data.
Creating a regular expression
A regular expression is an object. There are two ways you can create a regular expression in JavaScript. You can either use a RegExp()
constructor or you can enclose the pattern inside a pair of forward-slash (/
) characters.
let re1 = new RegExp("abc");
let re2 = /abc/;
Both of these examples describe the same pattern: a character a
followed by a b
followed by a c
. The second notation, however, treats backslash (\
) characters differently. For example, since the forward-slash denotes the pattern, if you want a forward-slash to be a part of the pattern, you need to put a backslash in front of it (\/
).
Matching patterns
Regular expression offers a handful of methods for us to use, the most commonly used one should be the test()
method, which is used for matching patterns in string data.
console.log(/abc/.test("abcde")); // → true
console.log(/abc/.test("abxde")); // → false
In this example, the test()
method will examine the string that is passed to it, and return a boolean value telling you if a pattern match is found.
However, simply testing if the pattern "abc"
is found in a string does not seem very useful. Sometimes we want to test for a match using a set of characters. For example, the following code test if at least one of the characters, from character 0 to character 9, exists in the string "in 1992"
.
console.log(/[0123456789]/.test("in 1992")); // → true
// A hyphen character can be used to indicate a range of characters
console.log(/[0-9]/.test("in 1992")); // → true
It is also possible to match any character that is not in the set. For example, this time we'll match any character that is not 1 or 0.
let notBinary = /[^01]/;
console.log(notBinary.test("1100100010100110")); // → false
// The string contains a character "2" which is not in the set [01]
console.log(notBinary.test("1100100010200110")); // → true
Some of the commonly used character sets have shortcuts in regular expressions. For instance, \d
represents all digit characters, same as [0-9]
.
-
\d
Any digit character -
\w
Any alphanumeric character (word character) -
\s
Any whitespace character (space, tab, new line ...) -
\D
Any non-digit character -
\W
Any non-alphanumeric character -
\S
Any non-whitespace character -
.
Any character except for the new line
Now, we could match a date-time format (10-07-2021 16:06) like this, and as you see, it is awkward and cryptic as advertised.
let dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/;
console.log(dateTime.test("10-07-2021 16:06")); // → true
Matching repeating patterns
You may have noticed that in our previous example, each \d
only matches one digit character. What if we want to match a sequence of digits of arbitrary length? We can do that by putting a plus mark (+
) after the element we wish to repeat.
console.log(/\d+/.test("123")); // → true
console.log(/\d+/.test("")); // → false
The star sign has a similar meaning except it allows the element to match for zero times.
console.log(/\d*/.test("123")); // → true
console.log(/\d*/.test("")); // → true
We can also indicate precisely how many times we want the element to repeat. For example, if we put {4}
after an element, that means this element will be repeated four times. If we put {2,4}
after that element, it means the element will be repeated at least twice and at most four times.
console.log(/\d{3}/.test("123")); // → true
console.log(/\d{3}/.test("12")); // → false
It is possible to repeat a group of elements as well. We only need to enclose that group of elements inside a pair of parentheses.
let cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo")); // → true
In some cases, we need a part of the pattern to be optional. For example, the word "neighbour" can also be spelled "neighbor", which means the character "u" should be optional. Here is what we can do:
let neighbor = /neighbou?r/;
console.log(neighbor.test("neighbour"));
// → true
console.log(neighbor.test("neighbor"));
// → true
Other methods for matching patterns
The test()
method is the simplest way of finding out if a pattern match is found in a string. However, it doesn't give you much information besides returning a boolean value telling you if a match is found. The regular expression also has an exec()
method (exec
stands for execute) that would return an object giving you more information, such as what the match is and where it is found.
let match = /\d+/.exec("one two 100");
console.log(match); // → ["100"]
// The index property tells you where in the string the match begins
console.log(match.index); // → 8
There is also a match()
method that belongs to the string type, which behaves similarly.
console.log("one two 100".match(/\d+/)); // → ["100"]
The exec()
method can be very useful in practice. For example, we can extract a date and time from a string like this:
let [_, month, day, year] = /(\d{1,2})-(\d{1,2})-(\d{4})/.exec("1-30-2021");
The underscore (_
) is ignored, it is used to skip the full match that is returned by the exec()
method.
However, now we have another problem from this example. If we pass to the exec()
method a sequence of nonsense like "100-1-3000"
, it would still happily extract a date from it.
In this case, we must enforce that the match must span the entire string. To do that, we use the boundary markers ^
and $
. The caret sign (^
) marks the start of the string and the dollar sign ($
) matches the end of the string. So, for instance, the pattern /^\d$/
would match a string that only consists of one digit character.
Sometimes you don't want the match to be the entire string, a string could be a complete sentence and you want the match to be a word in that sentence. To mark a word boundary, we use the \b
marker.
console.log(/cat/.test("concatenate")); // → true
console.log(/\bcat\b/.test("concatenate")); // → false
The last type of pattern we must introduce is the choice pattern. Sometimes we don't want to match a specific pattern, but instead, we have a list of acceptable patterns. we can divide the different patterns using the pipe character (|
).
let animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.log(animalCount.test("15 pigs")); // → true
console.log(animalCount.test("15 pigchickens")); // → false
If you are interested in learning more about the regular expression, this is a very interesting website to play around.
Replacing a pattern
Besides the match()
method, string values also have a replace()
method that replaces part of the string with another string.
console.log("papa".replace("p", "m"));
// → mapa
The first argument of the replace()
method can also be a regular expression, in which case the first match of that regular expression will be replaced with the second argument. If you wish to replace all matches of the regular expression, add a g
option (global option) to that regular expression.
console.log("Borobudur".replace(/[ou]/, "a")); // → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a")); // → Barabadar
The canvas
Remember when we talked about HTML and CSS, we briefly introduced something called SVG? It allows us to create beautiful images by simply using HTML tags. Today, we are going to introduce something similar called canvas, except it allows us to use javascript to create graphics on web pages. And because it uses a programming language instead of a simple markup language, that makes canvas much more flexible and powerful compared to SVG.
We know that the SVG has a DOM tree structure, and the shape, color, and position are all represented using HTML tags. The canvas, however, is one single HTML node, but it encapsulates a space on the web page, where you can create beautiful artworks using JavaScript. This space can be defined using the <canvas>
tag. Here is an example where we create a simple rectangle inside the canvas space:
<canvas width="300px" height="200px"></canvas>
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
// Define the colour of the rectangle
context.fillStyle = "red";
// The first two parameters means that the top left corner of the ractagle is at coordinate (10, 10)
// The last two parameters define the width and height of the ractangle (width:100px, height:50px)
context.fillRect(10, 10, 100, 50);
</script>
The getContext()
method is used to access the drawing interface, which is like a toolbox where your digital pens and pencils are stored. The parameter "2d"
stands for two-dimensional graphics. If you are interested in creating three-dimensional graphics, you should use WebGL instead. But we are only focusing on the 2D system for now.
Also, notice that we defined the size of the canvas at the beginning. If you don't do that, the canvas element will take a default width of 300 pixels and a height of 150 pixels.
Lines
The rectangle we just created is solid, the inside of the rectangle is filled. What if we want something different? It is also possible for us to create a rectangle that only has an outline, by using a very similar method, strokeRect()
. This method also takes four parameters, the first two define the position and the last two define the size.
<canvas></canvas>
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
// Define the color, position and size
context.strokeStyle = "blue";
context.strokeRect(10, 10, 100, 50);
// Define the width of the strok and create a new rectangle
context.lineWidth = 5;
context.strokeRect(150, 10, 100, 50);
</script>
Paths
Now you might be wondering, that's not so exciting, we can create rectangles using SVGs just as easily. Don't worry, the real power of the canvas starts now.
First, we need to understand what a path is. A path is a sequence of line segments. For example, we have a line that starts from coordinate (0, 0) to (0, 50), the second line from (0, 50) to (80, 50), and the third line from (80, 50) to (80, 100). These three line segments will form a path.
The canvas allows us to do something like this:
<canvas width="500px" height="500px"></canvas>
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
context.lineWidth = 5;
context.strokeStyle = "green";
context.beginPath();
// The path starts at (10, 10)
context.moveTo(10, 10);
// Drawing the path: (10, 10) -> (150, 10) -> (150, 150) -> (10, 150) -> (10,10)
context.lineTo(150, 10);
context.lineTo(150, 150);
context.lineTo(10, 150);
context.lineTo(10, 10);
context.stroke();
</script>
With paths, we can create any shape we want. For example, the following code creates a triangle:
<canvas width="500px" height="500px"></canvas>
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
context.beginPath();
context.fillStyle = "red";
context.moveTo(200, 10);
context.lineTo(250, 100);
context.lineTo(150, 100);
context.lineTo(200, 10);
context.fill();
</script>
Curves
A path could be formed by straight lines, and it could also be formed by curves. A curve, however, is a little bit more difficult to define. To define a curve, we need a start point, a destination point, and a control point. The curve will not go through the control point directly, but instead, it defines a point where the tangent line of the start and destination point goes through.
This is a little hard to understand. You could get familiar with the pen tool in Photoshop or the path tool in GIMP first. They share the same concept, except when you are coding, you need to imagine what the curve looks like.
Here is another example. We'll first draw the curve, and then draw the tangent lines and the control point, so that it helps you understand what's going on here:
<canvas width="500px" height="500px"></canvas>
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
context.beginPath();
// start point = (10, 90)
context.moveTo(10, 90);
// control point = (60,10); destination point = (90,90)
context.quadraticCurveTo(60, 10, 90, 90);
// destination point tangent
context.lineTo(60, 10);
// start point tangent
context.moveTo(10, 90);
context.lineTo(60, 10);
context.closePath();
context.stroke();
</script>
Sometimes we want the start point tangent and the destination point to have different control points. That is also possible to achieve using the bezierCurveTo()
method.
<canvas width="500px" height="500px"></canvas>
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
context.beginPath();
// start point = (10, 90)
context.moveTo(10, 90);
// start control point = (60,10); destination control point = (30,80); destination point = (90,90)
context.bezierCurveTo(60, 10, 30, 80, 90, 90);
// destination point tangent
context.lineTo(30, 80);
// start point tangent
context.moveTo(10, 90);
context.lineTo(60, 10);
context.closePath();
context.stroke();
</script>
Texts
Texts might also be useful when we are creating graphs. We can draw texts using either fillText
and strokeText
. The latter will only render the outline of the texts instead of filling it.
<canvas width="1500px" height="500px"></canvas>
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
context.font = "28px Georgia";
context.fillText(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
10,
50
);
context.strokeText(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
10,
100
);
</script>
The last two parameters indicate the position of the text, but unlike drawing shapes, it defines the coordinate of the start of the text's baseline. The baseline is the line that the text stands on.
Transformations
There are primarily three types of transformations, translate()
, scale()
and rotate()
. Remember that these methods need to be put before the graph you wish to transform.
translation()
will move the graph from one position to another:
<canvas width="1500px" height="1500px"></canvas>
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
// Move whatever graph created after to the right for 50px and downward for 100px
context.translate(50, 100);
// Create a graph
context.beginPath();
context.fillStyle = "red";
context.moveTo(200, 10);
context.lineTo(250, 100);
context.lineTo(150, 100);
context.lineTo(200, 10);
context.fill();
</script>
The scale()
will make the original graph bigger or smaller:
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
// Make the graph 2 times wider (along x-axis) 0.5 time shorter (along y-axis)
context.scale(2, 1/2);
// Create a graph
...
</script>
And finally, rotate()
can rotate the graph along an axis:
<canvas width="1500px" height="1500px"></canvas>
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
// Rotate the graph clockwise for 18 degrees. Notice that the rotate() method takes radian instead of degree.
context.rotate(0.1 * Math.PI);
// Create a graph
...
</script>
Bitmap graphics
In computer graphics, there is something called vector graphics and bitmap graphics. All the graphs we've been talking about so far are vector graphics. Their primary difference is that the bitmap graphics are formed by pixels while the vector graphics are not. Instead, they are formed by paths, with a direction and a magnitude (length), like a vector.
However, it is necessary for us sometimes to insert some bitmap graphics in our vector graphic design. We can do that by using the drawImage()
method.
<canvas width="1500px" height="1500px"></canvas>
<script>
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
let img = document.createElement("img");
img.src = "cat.jpg";
img.addEventListener("load", () => {
context.drawImage(img, 10, 10, 360, 240);
});
</script>
In this example, the image will be drawn at the coordinate (10, 10), with the size 360px * 240px.
We need to add the event listener because, without it, the image will load after the canvas, so we have to make the canvas wait for the image to load first.
The network
If you've made it to this point, congratulations, we are done with the frontend basics of JavaScript. It is almost time for us to dive into the backend of web development. However, before we do that, we must talk about how the internet actually works.
The network, to put it simply, is multiple computers connected together. These computers can send information to each other. And if this network extends to the entire planet, it becomes what we call the internet.
Network protocols
When a computer (server) sends data and resources, it has to follow a certain protocol, so that the computer (client) that receives the resources knows how to read them. There are protocols for sending and receiving emails, sharing files, and even controlling another computer over the internet. We don't have time to introduce all of them, so instead, we'll focus on HTTP, HTTPS as well as the TCP protocol.
The TCP protocol is one of the most commonly used internet communication protocols, in fact, a lot of other protocols are created on top of it. It works as follows: one computer must always be listening, waiting for other computers to start talking to it.
This computer has different "listeners", and they can listen for different kinds of communications at the same time, to make sure these listeners don't interrupt each other, each of them will take up one position (port) on that computer. For example, when we receive emails, that email is sent to us using the SMTP protocol, which is created based on the TCP protocol. By default, our computer will always be listening on port 25 for emails.
For another computer to send data to the target computer, it needs to "talk" to the target computer through the correct port. If the target machine can be reached and is listening on that port, a connection will be established, and the data transfer can begin. In this case, the computer that is listening is called the client, and the computer doing the talking is called the server.
The Hypertext Transfer Protocol (HTTP) is a protocol for retrieving named resources. It means that the client would first make a request to the server, asking for some resources. The resources are usually web pages, images, or CSS/JavaScript files. If the server is OK with that request, it would return a 200 OK
message back to the client, and start transferring the files. The HTTP request sent by the client usually looks like this:
# Start with HTTP method (we'll discuss this in detail later),
# followed by the name of the resource, and the version of the protocol
GET /index.html HTTP/1.1
# You can also specify other information here
Host: example.com
Accept-Language: en
And the response looks like this:
# Start by the 200 OK message
HTTP/1.1 200 OK
# Some extra info here
Date: Sat, 09 Oct 2010 14:28:02 GMT
Server: Apache
Last-Modified: Tue, 01 Dec 2009 20:18:22 GMT
ETag: "51142bc1-7449-479b075b2891b"
Accept-Ranges: bytes
Content-Length: 29769
Content-Type: text/html
# The requested resource
<!DOCTYPE html... (here come the 29769 bytes of the requested web page)
Of course, when you are surfing the internet, you never actually had to do this manually, the browser does everything automatically for you when you type in the uniform resource locator (URL), which specifies the protocol, host, and the path to the resource you want.
http://example.com/2020/03/16/13_browser.html
| | | |
protocol server path
The HTTPS protocol works exactly the same, except it is encrypted. It uses something called the transport layer security (TLS) protocol to make sure that the communication between the client and the server is secure. The server has a private key and the client has a public key, the connection could only be established if the two keys match each other.
HTTP methods
Since we are focusing on web development, we’ll only discuss the HTTP protocol in detail. Notice that from our previous example, when we send an HTTP request, the request starts with a keyword GET
, which is called an HTTP method. There are six other methods besides GET
, and each of them serves a different purpose.
The GET
Method
The GET
method is the most commonly used HTTP request method. It is used to request data and resources from the server. When you send a GET
request, the query parameters are embedded in the URL in name/value pairs like this:
http://example.com/2020/03/16/13_browser.html?name1=value1&name2=value2
Note that the question mark (?
) marks the beginning of parameters, and the ampersand divides two different parameters.
The POST
Method
The POST
method is used to send data to the server, either adding a new resource or updating an existing resource. The parameters are stored in the body of the HTTP request.
POST /index.html HTTP/1.1
Host: example.com
name1=value1&name2=value2
The DELETE
Method
This one is very intuitive, it deletes a resource from the server.
The HEAD
Method
The HEAD
method works just like the GET
method. Except the HTTP response sent from the server will only contain the head and not the body. Meaning if the server is OK with the request, it will give you a 200 OK
response but not the resource you requested. You can only retrieve the resource with the GET method. This is very useful when testing if the server works.
THE PUT
Method
The PUT
method is similar to the POST
method, with one small difference. When you POST
a resource that already exists on the server, this action would not cause any difference, it would always produce the same result. The PUT
method, however, will duplicate that resource, every time you make the request.
HTML forms and HTTP
Now that we know what an HTTP request would look like, it is time to talk about how to send a request. The most common way of doing that is through HTML forms. It allows the user to fill out information and submit them as parameters. Here is an example:
<form method="GET" action="example/message.html">
<p>Name: <input type="text" name="name" /></p>
<p>Message:<br /><textarea name="message"></textarea></p>
<p><button type="submit">Send</button></p>
</form>
Let's first look at the <form>
tag. The method attribute specifies the HTTP method we are going to use. In this case, it's GET
, which means the parameters will be embedded inside the URL. The action
specifies the domain and the path to the file we are requesting. Usually, the server will perform some actions to that file based on the parameters you send, and return you a customized file.
If you look inside the <form>
element, notice that the user input elements (both <input>
and <textarea>
) have name
attribute. This defines the name of the parameter, which is a name/value pair. The corresponding value of that name would be the user input. This name
is very important, you have to make sure that when you are coding the backend, the names are consistent.
When you push the "Send" button, the HTTP request would look like this:
GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1
JavaScript and HTTP
Besides HTML forms, JavaScript can also be used to send HTTP request. It can be done using the fetch()
method like this:
fetch("path/to/resource").then((response) => {
// Get the returned response status (200 OK)
console.log(response.status);
// Get the header of the response
console.log(response.headers.get("Content-Type"));
});
By default, the fetch()
method uses GET
method to make the request, you can change that by specifying the method.
fetch("path/to/resource", {method: "POST"}).then(...);
Or adding extra information in the header, and add parameters in the body like this:
fetch("path/to/resource", {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
body: "name1=val1&name2=val2",
}).then(...);
However, using JavaScript to make HTTP requests does raise some security concerns. Because the user and the programmer aren't usually the same person, they might not have the same interest in mind. Obviously, you don't want a random web page to access your bank with credentials stored in your browser. This is why most browsers forbid JavaScript from making HTTP requests by default.
This can be very annoying because it is possible that the JavaScript code wants to access another domain for a legitimate reason. To solve this problem, the servers can include in the response saying that it is OK for the request to come from another domain.
Access-Control-Allow-Origin: *
JavaScript in the backend
Before we wrap up this tutorial, we are also going to discuss how JavaScript works as a backend languages. First of all, create a new folder on your computer. Make sure that all the installing, creating, and updating that we do in this in this section happens in this directory.
About Node.js
Node.js is a JavaScript runtime that allows us to run JavaScript on almost any platform, not just the browser. After you install Node.js on your system, you will have a program named node
, and you can use it to run JavaScript files like this:
node example.js
If the file example.js
contains the following code:
console.log("Hello, World!");
The output will be:
"Hello, World!"
This is very similar to what we've seen when JavaScript is executed in the browser environment.
Installing and managing packages
When you install Node.js on your system, a package manager called npm
would also be installed. Unless you are using Linux, in which case you'll need to install it separately. But if you are already using Linux, you probably already know what you are doing.
The primary use of npm is to download and manage JavaScript packages that are required by your application. A package is a piece of program that is written and published by someone else, and simply grab it and use it in your own application. For example, if you are building an app that requires the package called ini
, you can run the following command in the terminal. Make sure you are in the correct folder!
npm install ini
When you first run this command, npm
will create three different things in your working directory.
First, we have a new folder named node\_modules
, which stores the package you just installed. And there are also two JSON files, package.json
and package-lock.json
. Both of them are used for version control. Their difference is that the package-lock.json
stores the exact version of the package, while package.json
stores the minimum version that is required, as well as any other information about the app. You can easily tell their difference by comparing them side by side.
To use the package we just installed, invoke the require()
method.
const { parse } = require("ini");
// We can perform some actions using the variable parse
If you are interested in learning more about the npm
tool and how to manage packages with it, you can go to https://npmjs.org for more documentation. But for now, we don't need to know too much about it.
Before we can start building our backend app, there are two JavaScript packages you must understand, the http
module and the fs
module. We are going to use the http
module to create a server, and use the fs
module to read and write to a file, which we'll use as a database to store information.
The file system module
Let's first start with the file system (fs
) module. This package is built into Node.js, so we don't need to install anything in this case. Instead, we'll create a new .js
file for the code and a .txt
file for the JavaScript to read and write. We'll import the module as we talked about before.
// import the fs module
let { readFile } = require("fs");
// specify the file we want to read as well as the charset encoding format
readFile("data.txt", "utf8", (error, text) => {
// If there is an error reading the file
if (error) throw error;
// If no error, print the content of the file
console.log(text);
});
We can also write to the file like this:
const { writeFile } = require("fs");
writeFile("data.txt", "Hello, World? Hello, World!", (error) => {
if (error) console.log(`${error}`);
else console.log("File written.");
});
In this case, it is not necessary to specify the encoding format. If writeFile
is given a string, it will simply assume the default format, which is UTF-8.
The HTTP module
Another very important module we need to talk about is http
, it allows us to create an HTTP server using JavaScript. For example:
const { createServer } = require("http");
let server = createServer((request, response) => {
// If a request is recieved, return a 200 OK response along with some other information
response.writeHead(200, { "Content-Type": "text/html" });
// The body of the HTTP response
response.write(`<h1>Hello, World!</h1>`);
// The response ends
response.end();
});
// Make the HTTP server listen on port 8000
server.listen(8000);
console.log("Listening! (port 8000)");
The variables request
and response
each represent an object storing the incoming and the outgoing data. For instance, you can access the url
property of the request by using request.url
.
This example is very simple, but in reality, the backend servers are usually more complex. So next, let's try something more challenging. We are going to create a simple app that asks for your name, and once you submit your name, the data will be stored in a txt
file, which acts as a database. When you visit the web page again, it will greet you with your name.
A simple app
You can access the source code for this tutorial here 👈.
Build the server
Step one, we'll create a backend without worrying about the database. Let's create a new JavaScript file named server.js
:
const { createServer } = require("http");
let server = createServer((request, response) => {
request.on('data', function(){...});
request.on('end', function(){...});
});
server.listen(8000);
console.log("Listening! (port 8000)");
This is very similar to our previous example, but this time we'll use event listeners to configure the server. The first event we are listening to is data
, which means when the HTTP request is transmitting data. In this case, we should extract the information we need to use from the request. The second event is end
, which means when the request is not transmitting data, in this case, the server should respond with some information.
// Initialize the variable "name"
let name = "";
request.on("data", function (chunk) {
// "chunk" is the data being transferred
name = name + chunk;
// The data is in name/value pair (name1=value1)
// So, we need to split the name and the value
name = name.split("=");
});
request.on("end", function () {
response.writeHead(200, { "Content-Type": "text/html" });
// For now, we'll use the data directly without a database,
// just to test if the server works
response.write(`
<h2>Hello, ${name[1]}</h2>
<p>What is your name?</p>
<form method="POST" action="example/message.html">
<p>Name: <input type="text" name="name"></p>
<p><button type="submit">Submit</button></p>
</form>
`);
response.end();
});
Run the server with the following command:
node server.js
Open our browser and go to http://localhost:8000.
Submit your name and see if anything changes.
Build the database
However, this data is only temporary. It will be lost if you restart the server or refresh the browser. What if you want to store the data for a bit longer?
Now, we'll create a new file called data.txt
, and we'll use it to store the name you submitted.
const { createServer } = require("http");
const fileSystem = require("fs");
let server = createServer((request, response) => {
// To make things more clear, name is used when writing to file
// myName is used when reading from file
let name = "";
let myName = "";
request.on("data", function (chunk) {
name = name + chunk;
name = name.split("=");
name = name[1];
// Write the data to data.txt
fileSystem.writeFile("data.txt", name, function (error) {
if (error) throw error;
});
});
request.on("end", function () {
response.writeHead(200, { "Content-Type": "text/html" });
// Read the data from file
fileSystem.readFile("data.txt", "utf8", (error, text) => {
if (error) throw error;
myName = text;
});
response.write(`
<h2>Hello, ${myName}</h2>
<p>What is your name?</p>
<form method="POST" action="example/message.html">
<p>Name: <input type="text" name="name"></p>
<p><button type="submit">Submit</button></p>
</form>
`);
response.end();
});
});
server.listen(8000);
console.log("Listening! (port 8000)");
Notice the syntax when importing the packages. const { xxx } = require('xxx')
is importing a method from a package, and const xxx = require('xxx')
is importing the entire package, and we can access one of the methods using xxx.methodName
.
Run this server and resubmit your name, this time if you open the data.txt
file, you will see that the data has been written to the file.
Conclusion
And that's it, we've covered all the fundamental concepts in JavaScript. However, there is, in fact, a lot more.
If you are curious what is a pure function, what is a higher-order function, or what is the difference between object-oriented programming and functional programming, or if you want to know how to create a calculator, a drawing board, or a full stack application with user authentication features, take a look at our complete course on JavaScript!