Learning JavaScript | Control Flow & Scope
Control Flow is a fundamental concept in programming that dictates the sequence in which the program’s individual statements, instructions, or function calls are performed. It’s an important aspect of JavaScript and influences the language’s Lexical Scope.
Anatomy of a Conditional
Before we begin id like to cover some ground about how conditionals are written in JavaScript, and some of the features they come with.
There are two types of conditionals, the if else block and the switch statement
If-Else Block
if (condition) { // Conditions must return, or be of boolean type
// Do something
}else {
// Do this only if all the above return false
}As we have seen before here is a simple if-else block
if(2 < 3 ) { // results to a boolean value True, because 2 is less than 3
console.log("This is true");
}else {
console.log("This is false")
}The example above is very simple and will always return true => This is true, let's write a more realistic example
const namesInHat = ["Osiel", "Justin", "Tias", "Faith"];
const randomNumber = Math.floor(Math.random() * namesInHat.length + 1);
if (randomNumber === 3) { // === is a strict equality checker, checks if two values are the same type and value
console.log(namesInHat[randomNumber]);
} else if (randomNumber === 0) {
console.log(namesInHat[randomNumber]);
} else if (randomNumber === 1) {
console.log(namesInHat[randomNumber]);
} else if (randomNumber === 2) {
console.log(namesInHat[randomNumber]);
}else {
console.log("None of those match my list, try again")
}Ok, maybe this isn’t the most realistic example, since there are better more efficient ways to make a random hat name picker. But in this example, we get to set our top if condition, our else if conditions, similar to the else block they run:
- only if the previous condition is false
- and are attached to the original if the condition
and then we set our else block in the case that we pull a number that does not exist in our array.
One more thing about if blocks
the condensed block
if (2 < 3) console.log("this is true")This works best for cases where you want to quickly check if something is true, without writing a whole block, but as you see, you cannot explicitly declare and else or else-if block
Switch Statement
switch (case) { // A value that we will later use to compare against in our cases
case condition: // a case is like an if or else-if block, and the condition is the thing we are comparing against
//Do something
break; // The break keyword moves the execution along in the case that a value is true
default: // same as the else block
// Do this only if all the above return false
}Here is a simple example using a switch statement:
switch (2) { // We will compare other values against this
case 3: // => is 2 === 3 ? No
console.log("not equal");
break;
case 2: // => is 2 === 2 ? Yes
console.log("Equal"); // Print this
break; // Break out of this block and return to normal execution
default:
console.log("Non were equal");
}We can see switch statements are smaller, and a bit easier to read. We mostly use them for occasions where we have a lot of conditions to compare, instead of having large blocks for each, we instead write cases under a switch statement. Now let’s write the realistic example from before using a switch statement
const namesInHat = ["Osiel", "Justin", "Tias", "Faith"];
const randomNumber = Math.floor(Math.random() * namesInHat.length + 1);
switch (randomNumber) {
case 3:
console.log(namesInHat[randomNumber]);
break;
case 0:
console.log(namesInHat[randomNumber]);
break;
case 1:
console.log(namesInHat[randomNumber]);
break;
case 2:
console.log(namesInHat[randomNumber]);
break;
default:
console.log("None of those match my list, try again")
}Much more condensed code, with the same functionality
Truthy Falsey
Truthy Falsey values are the name we give to how a conditional will determine if a value in the condition statement is true or false. A lot of time when learning about truthy falsey we start also learning about these operators:
|| // Or
&& // And
! // Not (also known as the "bang" symbol)
?? // Null Coalescing
.? // Optional Chaining
=== // Strict equality operator
== // Non-Strict equality operatorHere are some quick examples of how they work before we define Truthy Falsey:
if (2 < 3 || 2 > 4) console.log("This is true") // Because the || operator only checks if 1 condition is true
if (2 < 3 && 2 > 4) console.log("This is false") // Because the && operator needs both conditions to return true
if (2 == 2 && 2 == "2") console.log("This is True") // Because the == operator only check the value, not the typing
if (2 === 2 && 2 === "2") console.log("This is False") // Because the === operator checks the value and typingNow to dive deeper into how JavaScript determines this, we need to describe item precedence, very similar to how the order of operations works in math
This code snippet is from here: MDN Web Docs
a || (b * c); // evaluate `a` first, then produce `a` if `a` is "truthy"
a && (b < c); // evaluate `a` first, then produce `a` if `a` is "falsy"
a ?? (b || c); // evaluate `a` first, then produce `a` if `a` is not `null` and not `undefined`
a?.b.c; // evaluate `a` first, then produce `undefined` if `a` is `null` or `undefined`The way to read this is like so:
- The || operator looks to the left first, if this value is true it won’t look to the right, it will return True
- The && operator will look to the left first, and if it is false it will return false, ignoring what’s on the right, if the first value on the left is true, it will look at the value on the right, and only that is true will it return true
- the ?? operator will see check if the value on the left is
nullorundefinedfirst, if so it will return the value on the right. If not then it will check if the value on the right isnullorundefinedand return the value on the left. - The ?. will look to the left first, if a is anything other than
nullorundefinedit will check if b is true, and so on.
Here is a link with a table describing the precedence of each operator MDN Web Docs
Short Circuiting
We have already explored the concept of short-circuiting, we can think of it as something happening that makes an action jump out of its standard execution. Example:
Get Harold’s stories in your inbox
Join Medium for free to get updates from this writer.
For the ||(OR) operator our variable a is short-circuiting. Because the || operator expects one of the two values to be true to return that value. Once it checks the left, the program has no need to check the right, so it just stops and checks the value on the left
const a = true || "Hello";
const b = false || "Hello";
console.log(a); // returns true
console.log(b); // returns "Hello"For the &&(AND) operator our variable c is short-circuiting. Because the && operator expects both values to be true. Once it checks the left, it has no need to check the right, so it just stops and returns the value on the left.
const c = false && "";
const d = true && "Hello";
console.log(c); // Returns false
console.log(d); // Returns "Hello"Procedural Execution & Global Scope
JavaScript is one of those languages that has a lack of “boiler plate” code to get you started. When you want to write a program in JavaScript you can just get started writing in the Global Scope. And by default JavaScript will start reading and executing your code from top to bottom.
console.log("Hello")
console.log("World")
console.log("How")
console.log("Are")
console.log("You?")Returns:
// Hello
// World
// How
// Are
// You?And we can see the order in which each one of these words is logged into our console when running this program in VS Codes Debug Mode. If you notice the terminal (Debug Console) is logging each line of code in the order its written. Using the same example, I want to show what happens when we declare a function and add some complexity to our control flow.
// code above remains the same
// define variable and attach the string "John" to it
const firstName = "John";
// define a function that prints out a string
function printGreeting(greeting) {
console.log(greeting);
}
printGreeting("Hello!")
console.log(john)When we declare a function in the middle of our code, we skip over the declaration of the function until we call or invoke the function. So this execution will actually
- Declare the function first (although not something that we can see in the GIF)
- Create a variable firstName and assign it the value “John”
- Move to line 9 where we call(invoke) the function
- It goes back up to the function body on line 5
- Runs the
console.log(greeting) - Then we move on to the next line from when we called the function line 10.
Conditional Execution & Lexical Scope
Conditionals alter the path our code takes when executing, the example here is small and simple as an introduction but ill dive into two more examples later.
- Start at the top and by default run code from top to bottom, so “Hello from the outside” will be printed
- Our first condition, if
2 < 3then and only then, we printHello, 2 is less than 3otherwise we just skip this code entirely - Our second condition, if
2 > 3then we print2 is greater than 3but we know this can never be true, so we skip this block and go to theelsestatement. else is not another option, its the option that is executed when nothing before it returned true — think of it as the default execution path, so we will print2 will never be greater than 3! - End by printing “ok bye”
console.log("Hello from the outside");
if (2 < 3) {
console.log("Hello, 2 is less than 3");
}
if (2 > 3) {
console.log("2 is greater than 3");
} else {
console.log("2 will never be greater than 3!");
}
console.log("ok bye");The concept of lexical scoping, especially when it involves closures as in this example, has a profound impact on the control flow of a program. It allows functions to ‘remember’ their environment, and in doing so, it changes the lifetime of variables, allowing them to persist and be accessed even after their parent function has completed execution. This can enable more complex control flows. Lets look a simple example of Lexical Scope.
// Define a function named "numberChecker" that accepts two arguments, 'a' and 'b'.
function numberChecker(a, b) {
// Here we're creating a closure. This is a function that 'remembers' the environment
// in which it was created. In this case, it will remember the values of 'a' and 'b'.
return function checkAgainstC(c) {
// This function accepts a third number 'c' and compares it with 'a' and 'b'.
// If 'a' is less than 'b', log a message to the console.
if (a < b) {
console.log(`Hello, ${a} is less than ${b}`);
}
// If 'a' is greater than 'b', log a different message to the console.
else if (a > b) {
console.log(`Hello, ${b} is less than ${a}`);
}
// If 'c' is greater than both 'a' and 'b', log yet another message.
else if (c > a && c > b) {
console.log(`Woah! ${c} is greater than both ${a} and ${b}`);
}
// Here we're creating another closure. This one will remember the values of 'a', 'b', and 'c'.
return function greetWithAllNumbers(d) {
// This function accepts a fourth number 'd' and logs a message that includes all four numbers.
console.log("Hello from the inside! These are the numbers you've given me: ", a, b, c, d);
};
};
}Think about it like a series of rooms in a house. When we’re standing in the numberChecker() function, it’s as if we’re in the first room. In this room, we can see and use the variables ‘a’ and ‘b’. These are like objects in our first room.
Now, when we move to the checkAgainstC(c){} function, it’s as if we’re stepping into a second room that’s connected to the first one. In this second room, we can still see and use the objects — the variables ‘a’ and ‘b’ — from the first room. But now, we also have a new object, the variable ‘c’, which is unique to the second room.
Finally, we step into the third room — the greetWithAllNumbers(d){} function. Here, we can still see and use all the objects from the first and second rooms — the variables ‘a’, ‘b’, and ‘c’. And we have another new object, the variable ‘d’, that is only available in this third room.
This is what we call Lexical Scoping in JavaScript. As we go deeper into the house, into more nested functions, we carry with us the objects from the previous rooms. But if we tried to go backwards, say from the first room to the third, we wouldn’t be able to see or use the objects from the later rooms. Each room only knows about the objects from the rooms we’ve been in before, not the ones we’ll go to later. This is why the numberChecker() function only has access to ‘a’ and ‘b’, but greetWithAllNumbers(d){} has access to ‘a’, ‘b’, ‘c’, and ‘d’.
Summary
Control flow refers to the sequence in which individual statements, instructions, or function calls are executed in a program. We discuss conditional structures such as if-else blocks and switch statements, which are crucial for controlling the flow of a program based on certain conditions. It introduces truthy and falsy values, as well as various operators used in conditional statements. And Short-circuiting, a concept related to how logical operators function.
Procedural execution and global scope are discussed, where JavaScript reads and executes code from top to bottom





