DEV Community

Cover image for JavaScript Fundamentals
Eric Hu
Eric Hu

Posted on • Originally published at thedevspace.io

JavaScript Fundamentals

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:

Developer Tools Console

Or you can install Node.js on your computer, which allows you to run JavaScript programs using command terminals.

Node.js

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
Enter fullscreen mode Exit fullscreen mode

The primary usage of numbers is to perform arithmetic operations.

3 + 5 * 2; // -> 13
Enter fullscreen mode Exit fullscreen mode

Just like you studied in elementary school, multiplications and divisions happen first. However, you can change this by using parentheses.

(3 + 5) * 2; // -> 16
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.";
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

The output text would look like this:

This is the first line
This is the second line
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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}`;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The ! operator denotes logical not, and it flips the given value.

console.log(!true); // -> false

console.log(!false); // -> true
Enter fullscreen mode Exit fullscreen mode

We can also mix arithmetic operations with comparisons and logical operations.

1 + 1 == 2 && 1 + 1 < 0;
Enter fullscreen mode Exit fullscreen mode

In this example, 1 + 1 == 2 gives us true, and 1 + 1 < 0 gives us false, so we have

true && false; // -> false
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
let num = 10;
num = num - 5;

console.log(num); // -> 5
Enter fullscreen mode Exit fullscreen mode

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.

console.log()

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);
Enter fullscreen mode Exit fullscreen mode

Prompt Dialog

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));
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

For Loop

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

Do While Loop

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A function can have more than one parameter or no parameters at all (an empty set of parameters).

const sleep = function () {
  console.log("zzzzzzzzzzzzzzzzzzzzzz");
};
Enter fullscreen mode Exit fullscreen mode
var multiply3 = function (x, y, z) {
  return x * y * z;
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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).

const variable

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"));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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).

Recursion

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"];
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

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.

Stack

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); // -> []
Enter fullscreen mode Exit fullscreen mode

Stack push and pop

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.

Queue

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();
Enter fullscreen mode Exit fullscreen mode

enqueue and dequeue

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"
Enter fullscreen mode Exit fullscreen mode

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"],
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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.");
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

Prototypes

Look at the following code:

// Create an empty object
let empty = {};

console.log(empty.toString); // -> function toString(){...}
console.log(empty.toString); // -> [object Object]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!'
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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}'`);
  }
};
Enter fullscreen mode Exit fullscreen mode

To create an instance of this class, we can use the keyword new.

let killerRabbit = new Rabbit("killer");
let blackRabbit = new Rabbit("black");
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Each HTML element can be seen as a box. For example, the example document has the following structure:

HTML DOM

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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/;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

There is also a match() method that belongs to the string type, which behaves similarly.

console.log("one two 100".match(/\d+/)); // β†’ ["100"]
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Canvas Line Rectangle

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

canvas triangle

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>
Enter fullscreen mode Exit fullscreen mode

Curve

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>
Enter fullscreen mode Exit fullscreen mode

Curve with two control point

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>
Enter fullscreen mode Exit fullscreen mode

Texts in Cnavas

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"));
});
Enter fullscreen mode Exit fullscreen mode

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(...);
Enter fullscreen mode Exit fullscreen mode

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(...);
Enter fullscreen mode Exit fullscreen mode

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: *
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If the file example.js contains the following code:

console.log("Hello, World!");
Enter fullscreen mode Exit fullscreen mode

The output will be:

"Hello, World!"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

When you first run this command, npm will create three different things in your working directory.

Simple Node App

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.

package.json

To use the package we just installed, invoke the require() method.

const { parse } = require("ini");
// We can perform some actions using the variable parse
Enter fullscreen mode Exit fullscreen mode

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.

Create new file

// 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);
});
Enter fullscreen mode Exit fullscreen mode

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.");
});
Enter fullscreen mode Exit fullscreen mode

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)");
Enter fullscreen mode Exit fullscreen mode

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)");
Enter fullscreen mode Exit fullscreen mode

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("=");
});
Enter fullscreen mode Exit fullscreen mode
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();
});
Enter fullscreen mode Exit fullscreen mode

Run the server with the following command:

node server.js
Enter fullscreen mode Exit fullscreen mode

Open our browser and go to http://localhost:8000.

Undefined

Submit your name and see if anything changes.

With Name

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)");
Enter fullscreen mode Exit fullscreen mode

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.

data.txt

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!

Top comments (0)