Learning web development: Booleans, comparisons and if statements

[2025-08-20] dev, javascript, learning web dev
(Ad, please don’t block)

This blog post is part of the series “Learning web development” – which teaches people who have never programmed before how to create web apps with JavaScript.

To download the examples, go to the GitHub repository learning-web-dev-code and follow the instructions there.

I’m interested in feedback! If there is something you don’t understand, please write a comment at the end of this page.


In this chapter, we learn about tools for only running a piece of code if a condition is met: truth values (booleans), comparisons and if statements.

A new data type: booleans (truth values)  

So far, we know two data types: numbers and strings. And both have many different values: 0, -5, 7.1, etc.

Booleans are different – the only boolean values are true and false. They are returned by a function or method if it answers a yes-no question – e.g., the method Number.isInteger() tells us if its argument is an integer or not:

> Number.isInteger(1)
true
> Number.isInteger(5.0)
true
> Number.isInteger(5.1)
false

Each string has a method .startsWith() that tells us if it starts with the string provided as the argument:

> 'How are you?'.startsWith('How')
true
> 'How are you?'.startsWith('Why')
false

Each array has a method .includes() that tells us if the array has an element that is equal to the argument:

> ['a', 'b', 'c'].includes('a')
true
> ['a', 'b', 'c'].includes('x')
false

JavaScript has two kinds of values: primitive values and objects  

Before we can learn more, we need to explore a little bit of theory – because that helps us understand what comes later.

JavaScript makes an important distinction:

  • On one hand, there are primitive values such as undefined, booleans, numbers, and strings.
  • On the other hand, there are objects such as arrays.

Primitive values  

A primitive value is:

  • Atomic: It contains a single value.
  • Immutable: It can’t be changed, only replaced. For example, the following code does not change the number that’s stored in the variable num – it puts a new number into the variable:
    let num = 0;
    num = num + 1;
    
  • Compared by value: Two primitive values are equal if their contents are equal. We’ll learn later what exactly that means.

If we assign a primitive value to a variable, it is stored in that variable.

Objects  

An object is:

  • Compound: It contains zero or more values (primitive values or objects).
  • Mutable: It can be changed – e.g.:
    > const arr = ['a', 'b', 'c'];
    > arr[1] = 'x';
    > arr
    [ 'a', 'x', 'c' ]
    
    We didn’t put a new value into the variable arr, we modified its current value.
  • Compared by identity: Two objects are equal if their identities are equal. We’ll learn later what exactly that means.

If we assign an object to a variable, it looks like that object is store in that variable. And it’s usually good to pretend that that’s what happens. However, what actually happens is different – e.g.:

const arr = ['a', 'b', 'c'];
  • A new array is created and stored on the heap – a memory that is separate from variables.
  • The array has a unique identity (think passport number, account number, etc.). That identity is stored in the variable arr.
  • When we do things such as accessing array elements, JavaScript transparently (without us noticing) uses the ID stored in the variable to find the array on the heap and access it.

Assigning shares objects  

Why does JavaScript store identities in variables? Objects can potentially be quite large. Assignment copies values and we want to avoid copying large amounts of memory. That’s the benefit of using identities for objects: The following assignment only copies the identity of an array, not the array itself:

const arr1 = ['a', 'b', 'c'];
const arr2 = arr;

Interestingly, arr1 and arr2 now refer to the same array. They share that array. Therefore, if we change the array that arr1 refers to, we also change the array that arr2 refers to – because it’s the same array and because we don’t change the variables, we only change what they refer to:

> arr1[1] = 'x';
> arr1
[ 'a', 'x', 'c' ]
> arr2
[ 'a', 'x', 'c' ]

This is what the variables look like immediately after the assignment:

The diagram shows two different kinds of memory (RAM):

  • Left-hand side: variables
  • Right-hand side: heap

Both variables refer to the same array on the heap.

Assigning copies primitive values  

With primitive values such as numbers, we change the variables (which is why we can’t use const and have to use let):

> let num1 = 123;
> let num2 = num1;
> num2
123

> num1 = 0;
> num2
123

This is what the variables look like immediately after the assignment:

This time, there is nothing on the heap. The variables are enough for storing numbers.

Comparing via the strict equality operator ===  

The strict equality operator === tells us if its operands are equal or not.

As an aside, there is also a lenient equality operator == but that one has many pitfalls and my recommendation is not to use it – especially if you are new to JavaScript.

Primitive values: comparing the content  

If the operands of === are primitive values, they are compared by looking at their contents – which is also how humans would handle this task:

> 1 === 1
true
> 1 === 2
false

> 'hello' === 'hello'
true
> 'bye' === 'hello'
false

> true === true
true
> true === false
false

This way of comparing is called comparing by value.

Objects: comparing the identities  

If the operands of === are objects, their identities are compared – e.g.:

> const arr1 = [];
> const arr2 = arr1;
> arr1 === arr2
true

In this case, arr1 and arr2 refer to the same object, which is why === considers them to be equal.

In the next example, we compare two arrays that humans would consider to be equal (they have the some content). However, they are not equal to === because it compares identities and its operands have different identities:

> [] === []
false

This way of comparing is called comparing by identity.

Example: detecting empty strings via ===  

Let’s use === to implement a function that detects if a string is empty:

const isEmpty = (str) => {
  return str === '';
};

This is the function is action:

> isEmpty('')
true
> isEmpty('abc')
false

Comparing via <, <=, >, >=  

JavaScript also has operators that correspond to mathematical operators:

  • < corresponds to < (“less than”)
  • <= corresponds to ≤ (“less than or equal to”)
  • > corresponds to > (“greater than”)
  • >= corresponds to ≥ (“greater than or equal to”)

Numbers: numeric comparison  

Numbers are compared numerically – there shouldn’t be any surprises here:

> 3 < 3
false
> 3 <= 3
true
> 4 > 2
true
> 4 >= 2
true

Strings: lexicographical comparison (think dictionaries)  

Strings are compared lexicographically – similarly to how dictionaries order their words:

> 'apple' < 'banana'
true

This is sometimes useful, but has issues such as uppercase letters being greater than lowercase letters:

> 'apple' < 'Banana'
false

This kind of comparison doesn’t handle accents (é) and umlauts (ö) well, either. Therefore, we need to use other tools if we truly want to order things like in a dictionary. That isn’t a simple task because the rules are different for each human language.

Two operators for booleans: logical And (&&) and logical Or (||)  

For numbers, we have operators such as * and +. For booleans, we have operators such as && and ||. One one hand, these boolean operators follow very simple rules (which we’ll see soon). On the other hand, it’s fascinating how intuitive they are if their operands are expressions themselves.

Logical And (&&)  

Lets consider how human language handles “and”:

  • True: “The number 3 is greater than 0 and less than 9.”
  • False: “The number 3 is greater than 5 and less than 9.”

JavaScript’s logical And operator && works the same:

> const num = 3;
> (num > 0) && (num < 9)
true
> (num > 5) && (num < 9)
false

How is the last expression computed? JavaScript first computes the results of the operands and feeds those results to &&:

> num > 5
false
> num < 9
true
> false && true
false

The following table describes the results of && for all possible inputs:

op1 op2 op1 && op2
true true true
false false false
true false false
false true false

These results make intuitive sense: In English, “A and B” is only true if both A and B are true.

Logical Or (&&)  

The human language “or” is more like an “either-or”. Compare:

  1. “She is tall or she speaks Spanish.”
  2. “Either she is tall or she speaks Spanish.”
  3. “She is tall and/or she speaks Spanish.”

I’d argue that humans consider #1 to be equivalent to #2. JavaScript’s logical Or operator is an “and/or”: op1 || op2 is true if at least one of the operands is true.

| op1 | op2 | op1 || op2 | |---------|---------|--------------| | true | true | true | | false | false | false | | true | false | true | | false | true | true |

Logical Not (!)  

Logical Not (!) returns the opposite of a boolean value:

> !true
false
> !false
true

Example: isBetween()  

The following function detects if a value is between (including) a lower boundary and (including) an upper boundary:

const isBetween = (lower, upper, value) => {
  return (value >= lower) && (value <= upper);
};

This is the function in use:

> isBetween(0, 5, 1) // Is 1 between 0 and 5?
true
> isBetween(0, 5, 6) // Is 6 between 0 and 5?
false

We also could have used ! and || to implement this function:

const isBetween = (lower, upper, value) => {
  return !((value < lower) || (value > upper));
};

Excursion: the number error value NaN  

Let’s quickly learn something that we’ll need in the next section: Numbers have the error value NaN (which means “not a number”). NaN indicates that something went wrong – e.g., if Number() can’t convert a string to a number, it returns NaN:

> Number('abc')
NaN

Another is example is Math.sqrt() returning NaN whenever a number doesn’t have a square root that is a real number:

> Math.sqrt(-1)
NaN

If we want to check if a value is NaN, we unfortunately can’t use ===:

> NaN === NaN
false

However, the method Number.isNaN() lets us perform that check:

> Number.isNaN(NaN)
true
> Number.isNaN(4)
false

if statements  

One or two if branches  

So far, boolean values were only moderately useful. However, with if statements, they can control whether or not a piece of code is executed. This is the syntax of an if statement:

if (bool) {
  // Only executed if `bool` is `true`
}

This if statement only manages a single piece of code whose name is:

  • true branch
  • if branch

However, we can use else to add a second branch to the if statement:

if (bool) {
  // Only executed if `bool` is `true`
} else {
  // Only executed if `bool` is `false`
}

The name of the second branch is:

  • false branch
  • else branch

The following function describes if we are happy:

const iAmHappy = (bool) => {
  if (bool) {
    return 'I am happy! 🙂';
  } else {
    return 'I am not happy! ☹️';
  }
};

Let’s use that function:

> iAmHappy(true)
'I am happy! 🙂'
> iAmHappy(false)
'I am not happy! ☹️'

More if branches  

We can use else if to add more branches:

if (bool1) {
  // `bool1` is `true`
} else if (bool2) {
  // `bool1` is `false` and `bool2` is `true`
} else {
  // `bool1` is `false` and `bool2` is `false`
}

The following function uses three if branches:

const describeNumber = (num) => {
  if (Number.isNaN(num)) {
    return 'Not a number';
  } else if (Number.isInteger(num)) {
    return 'An integer';
  } else {
    return 'A floating point number';
  }
};

Let’s try it out:

> describeNumber(NaN)
'Not a number'
> describeNumber(4)
'An integer'
> describeNumber(4.1)
'A floating point number'

Note that return always immediately leaves a function. Therefore, we could also have implemented describeNumber() like this:

const describeNumber = (num) => {
  if (Number.isNaN(num)) {
    return 'Not a number';
  }
  if (Number.isInteger(num)) {
    return 'An integer';
  }
  return 'A floating point number';
};

Project: describe-input.html  

This project provides a user interface for the function describeNumber() from the previous section.

The input for describeNumber() comes from an <input> HTML element:

<input type="text">

The output of describeNumber() is written to a <p> HTML element:

<p id="description"></p>

The following JavaScript code controls the user interface:

const description = document.querySelector('#description');
document.querySelector('input')
  .addEventListener(
    'change',
    (event) => {
      const str = event.target.value;
      const num = Number(str);
      description.innerText = describeNumber(num);
    }
  );

Whenever a 'change' happens, the event listener retrieves the input, feeds it to describeNumber() and writes the result to the HTML element whose ID is 'description'.

Exercise (without solution)  

  • describe-input.html: Change describeNumber() so that it distinguishes between:
    • positive and negative integers
    • positive and negative floating point numbers

Project: number-guessing-game.html  

This game “thinks” of a number and the user has to guess which one it is. The game helps by telling the user if a guess is correct, too high or too low.

The game gets its input via a text field:

<input type="number" id="guessedNumberInput" disabled>

The input is submitted via a button:

<button id="guessButton" disabled>Guess:</button>

The output is displayed inside a <p> HTML element:

<p id="feedback"></p>

Note that both <input> and <button> are initially disabled: We don’t want users to submit answers until the game has started.

The following function starts the game (it is triggered by button that is not shown here):

let randomNumber = undefined;
const startGame = () => {
  randomNumber = getRandomIntegerBetween(LOWER, UPPER + 1);
  // Remove feedback from previous game (if there was one)
  feedback.innerText = '';
  guessButton.disabled = false;
  guessedNumberInput.disabled = false;
};

The constants LOWER and UPPER are defined elsewhere and determine the range of the number to be guessed. Function getRandomIntegerBetween() is shown later.

We are now ready to receive input from the user, which is why we enable guessButton and guessedNumberInput.

Whenever a new guess is submitted by a user, the following function is called:

const continueGame = (guessedNumber) => {
  //----- Error checks -----
  if (Number.isNaN(guessedNumber)) { // (A)
    feedback.innerText = '❌ Your guess must be a number!';
    return;
  }
  if (guessedNumber < LOWER || guessedNumber > UPPER) {
    // Parentheses are not required but help with readability
    feedback.innerText = (
      '❌ Your guess must be ' +
      'at least ' + LOWER + ' and ' +
      'at most ' + UPPER + '.'
    );
    return;
  }

  //----- Guess is valid – process it -----
  if (guessedNumber === randomNumber) {
    feedback.innerText = '✅ Your guess is correct!';
    guessButton.disabled = true;
    guessedNumberInput.disabled = true;
  } else if (guessedNumber < randomNumber) {
    feedback.innerText = 'Your guess is smaller than my number.';
  } else {
    // guessedNumber > randomNumber
    feedback.innerText = 'Your guess is larger than my number.';
  }
};

The user might input a non-number such as the text “abc”. The input is converted to a number via Number() – which is why the check in line A protects us from such a case:

> Number('abc')
NaN

After checking if the guessed number is within bounds, we give feedback to the user. If they have guessed correctly, then the game is finished. We disable guessButton and guessedNumberInput so that no further inputs are sent to continueGame().

Setting up LOWER and UPPER  

The HTML contains the following paragraph:

<p>
  I’m thinking of a number between
  <span id="lower">0</span> and
  <span id="upper">99</span>.
</p>

The JavaScript code extracts these values as follows:

  const LOWER = Number(document.querySelector('#lower').innerText);
  const UPPER = Number(document.querySelector('#upper').innerText);

Function getRandomIntegerBetween()  

You don’t have to understand how the following function works, but it may be interesting to try:

/** Returns a random integer i with min <= i < max */
const getRandomIntegerBetween = (min, max) => {
  return min + Math.floor(Math.random() * (max - min));
};

It is easier to understand if we start with a simpler version of it that we have already encountered:

/** Returns a random integer i with 0 <= i < max */
const getRandomInteger = (max) => {
  return Math.floor(Math.random() * max);
};

Steps performed by getRandomIntegerBetween():

  • It first does what getRandomInteger() does: It computes a random number in the range [0, max - min) (square bracket means “included”; parenthesis means “excluded”). This range has the same “length” as [min, max).
  • It then moves the result “into place”, by adding min to it:
    • If the previous step produced the number 0 then the final result will be min.
    • If the previous step produced the number max - min - 1 then the final result will be max - 1.
    • Etc.

Exercises (without solutions)  

  • number-guessing-game.html: Remove the button and retrieve input via 'change' events (which are triggered by users pressing Return).

  • number-guessing-game.html: Change this game so that the user has to guess letters between “a” and “z”. Make sure that the input is only a single character and within range. You can use the property .length for that purpose.