Javascript 101: Everything You Need To Know Before Learning React

Javascript 101: Everything You Need To Know Before Learning React

Table of contents

Are you ready to take your JavaScript skills to the next level? In this article, we'll explore 16 exciting topics that will help you become a JavaScript ninja!

We'll start with the fundamentals, like mastering variables and data types, and then move on to more advanced concepts like higher-order functions, closures, and asynchronous programming. Whether you're new to programming or a seasoned pro, this article will guide you on your journey to becoming a skilled React developer.

So, get ready to dive into the world of JavaScript with me. By the end of this article, you'll have the knowledge and confidence to start learning React. Let's get started!

1. Variables

Think of variables as labelled mailboxes. They're containers that hold messages (values). In JavaScript, we have three ways to set up a mailbox: let, const, and var. Each has its purpose, and knowing when to use what is vital for writing clean and efficient code.

1. let

Use let to declare a variable that you may reassign later. It's like a reusable grocery bag - you can put different items in it whenever you need to.

let weather = "sunny";
weather = "rainy";
console.log(weather); // "rainy"

2. const

Use const to declare a variable with a value that won't change. It's like your favourite coffee mug - it always holds your morning caffeine fix and nothing else.

const pi = 3.14159;
console.log(pi); // 3.14159

3. var

var is the grandparent of variable declarations. It comes from a time before let and const arrived (pre-ES6). var has different scoping rules, and it's a bit forgetful at times. It's best to let it retire and use let and const instead.

var pet = "dog";
console.log(pet); // "dog"

Temporal Dead Zone (TDZ)

The Temporal Dead Zone is like waiting in line at a concert. You're there, but you can't enter the venue and enjoy the music until the doors open. When you declare a variable with let or const, it enters the TDZ until it's initialized. Accessing it before initialization will result in an error.

console.log(favoriteFood); // ReferenceError: Cannot access 'favoriteFood' before initialization
let favoriteFood = "pizza";

2. Data Types

Data types in JavaScript can be divided into two categories: primitive and complex. Understanding these categories and their respective types will help you handle data more effectively.

1. Primitive data types

  1. string: Text strings are like sentences. They can be long, short, or even empty.

  2. number: Numbers are the building blocks of math. They're integers, decimals, and everything in between.

  3. boolean: Booleans are the binary code of life - either true or false, like a yes or no question.

  4. null: A null value represents an intentional lack of value. It's like reserving a table at a restaurant but not ordering anything.

  5. undefined: An undefined value signifies that a variable hasn't been assigned a value yet. It's like an empty plate waiting to be filled.

2. Complex data types

  1. object: Objects are like dictionaries. They hold a collection of key-value pairs, where each word (key) has a definition (value).

  2. array: Arrays are like shopping lists. They store an ordered collection of items, each with its unique position (index).


3. Operators

Operators in JavaScript are used to perform various operations on values and variables. They can be classified into different categories based on their functionality.

1. Arithmetic operators

  1. +: Addition

  2. -: Subtraction

  3. *: Multiplication

  4. /: Division

  5. %: Modulus (remainder)

let sum = 3 + 4;
console.log(sum); // 7

let difference = 10 - 3;
console.log(difference); // 7

let product = 5 * 2;
console.log(product); // 10

let quotient = 21 / 7;
console.log(quotient); // 3

let remainder = 25 % 7;
console.log(remainder); // 4

2. Comparison operators

  1. ==: Equal to (checks value only). Like checking if two puppies are the same breed.

  2. ===: Strictly equal to (checks value and type). Like checking if two puppies are identical twins.

  3. !=: Not equal to (checks value only). Like checking if two people have different favourite colours.

  4. !==: Strictly not equal to (checks value and type). Like checking if two cups of coffee are different in both size and flavour.

  5. <: Less than. Like comparing the ages of two siblings.

  6. >: Greater than. Like comparing the heights of two basketball players.

  7. <=: Less than or equal to. Like checking if you have enough money to buy a snack.

  8. >=: Greater than or equal to. Like checking if you have enough experience for a job position.


console.log(5 == "5"); // true
console.log(5 === "5"); // false
console.log(7 != "7"); // false
console.log(7 !== "7"); // true
console.log(2 < 5); // true
console.log(8 > 3); // true
console.log(6 <= 6); // true
console.log(9 >= 4); // true

3. Logical operators

  1. &&: Logical AND. Like a bouncer, only lets values pass if they're all true.

  2. ||: Logical OR. A more lenient bouncer, allows values to pass if at least one is true.

  3. !: Logical NOT. The reality inverter flips true to false and vice versa.


console.log(true && false); // false
console.log(true || false); // true
console.log(!true); // false

4. Ternary operator(? :)

A shorthand way to write an if...else statement, often used for simple conditional assignments. It's like choosing between two outfits based on the weather.

let age = 21;
let canDrink = age >= 18 ? 'Yes' : 'No'; // 'Yes'

5. Nullish coalescing operator(??)

Returns the right operand if the left operand is null or undefined. Otherwise, it returns the left operand. It's useful for setting default values when a variable might be null or undefined. It's like having a backup plan for dinner if your first choice is closed.

let a = null;
let b = 'Hello, World!';
let result = a ?? b; // 'Hello, World!'

4. Control Structures

1. Conditional Statements

Conditional statements allow you to execute different blocks of code based on specific conditions. In JavaScript, there are three main conditional statements: if, else if, and else.

1. if statement

The if statement checks if a condition is true and then executes the code within its block.

let weather = "rainy";

if (weather === "rainy") {
  console.log("Bring an umbrella!"); // Output: "Bring an umbrella!"
}

2. else if statement

The else if statement checks another condition if the previous if condition was false.

let temperature = 15;

if (temperature > 25) {
  // This block won't run
  console.log("It's hot!");
} else if (temperature > 10) {
  // Output: "It's pleasant!"
  console.log("It's pleasant!");
}

3. else statement

The else statement runs when none of the previous conditions is true.

let score = 6;

if (score > 10) {
  // This block won't run
  console.log("Great score!");
} else if (score > 8) {
  // This block won't run
  console.log("Good score!");
} else {
  // Output: "Keep trying!"
  console.log("Keep trying!");
}

2. Loops

Loops are used to execute a block of code multiple times. JavaScript provides several loop constructs to handle different scenarios.

1. for loop

The for loop is used to run a block of code a specific number of times.

// Output: 0 1 2 3 4
for (let i = 0; i < 5; i++) {
  console.log(i);
}

2. while loop

The while loop continues running as long as the specified condition is true.

let count = 0;
while (count < 5) {
  console.log(count);
  count++;
}
// Output: 0 1 2 3 4

3. do...while loop

The do...while loop first executes the block of code and then checks the condition. This ensures that the block of code is executed at least once.

let i = 0;
do {
  console.log(i); // Output : 0 1 2 3 4
  i++;
} while (i < 5);

4. for...in loop

The for...in loop iterates over the enumerable properties of an object.

let person = {
  name: "Alice",
  age: 30,
  city: "New York"
};

// Output:
// name: Alice
// age: 30
// city: New York
for (let key in person) {
  console.log(`${key}: ${person[key]}`);
}

5. for...of loop

The for...of loop iterates over iterable objects like arrays, strings, and sets.

let fruits = ["apple", "banana", "orange"];

for (let fruit of fruits) {
  console.log(fruit); // Output: apple banana orange
}

6. switch statement

The switch statement evaluates an expression and executes the corresponding case block. It can be an alternative to a series of if...else if...else statements when dealing with multiple possible values.

let grade = "B";

switch (grade) {
  case "A":
    // This block won't run
    console.log("Excellent!");
    break;
  case "B":
    // Output: "Well done!"
    console.log("Well done!");
    break;
  case "C":
    // This block won't run
    console.log("Good job!");
    break;
  default:
    // This block won't run
    console.log("Keep trying!");
}

Keep in mind that you should use the break statement after each case block. If you don't, the code will "fall through" to the next case block, potentially causing unintended behaviour.


5. Functions

1. Function Declarations

A function declaration defines a function using the function keyword, followed by the function name, a list of parameters, and the function body. The function body is enclosed in curly braces {} and contains the code to be executed.

function greet(name) {
  console.log('Hello, ' + name + '!');
}

greet('John'); // Output: Hello, John!

2. Function Expressions

A function expression is a way to define a function by assigning it to a variable. It's an alternative to the function declaration.

const greet = function(name) {
  console.log('Hello, ' + name + '!');
};

greet('Jane'); // Output: Hello, Jane!

3. Arrow Functions

Arrow functions are a shorter syntax for writing function expressions. They use the => symbol to define a function.

const greet = (name) => {
  console.log('Hello, ' + name + '!');
};

greet('Steve'); // Output: Hello, Steve!

4. Function Scopes

In JavaScript, there are two types of function scopes: global scope and local scope. Variables defined outside of any function have a global scope and can be accessed by any code in the script. Variables defined inside a function have a local scope and can only be accessed within that function.

const globalVar = 'I am global!';

function displayScope() {
  const localVar = 'I am local!';
  console.log(globalVar); // Output: I am global!
  console.log(localVar);  // Output: I am local!
}

displayScope();
console.log(globalVar); // Output: I am global!
console.log(localVar);  // Error: localVar is not defined

5. Immediately Invoked Function Expressions (IIFE)

An IIFE is a function expression that is defined and invoked immediately after its creation. It's a way to execute a function once and keep its variables and functions private.

(function() {
  const localVar = 'I am private!';
  console.log(localVar); // Output: I am private!
})();

console.log(localVar); // Error: localVar is not defined

6. Parameters and Arguments

Parameters are the names listed in the function definition, while arguments are the values passed to the function when it's called.

function sum(a, b) { // 'a' and 'b' are parameters
  return a + b;
}

console.log(sum(3, 4)); // 3 and 4 are arguments, Output: 7

7. Default Parameters

Default parameters allow you to set default values for function parameters, which will be used if the function is called without providing an argument for that parameter.

function greet(name = 'Stranger') {
  console.log('Hello, ' + name + '!');
}

greet('Mike');      // Output: Hello, Mike!
greet();            // Output: Hello, Stranger!

8. Rest Parameters and Spread Syntax

Rest parameters allow a function to accept an indefinite number of arguments as an array. It's denoted by using three dots ... before the parameter name.

function displayItems(...items) {
  console.log(items);
}

displayItems('apple', 'banana', 'orange'); // Output: ['apple', 'banana', 'orange']

Spread syntax is used to expand elements of an iterable (like an array) in places where multiple arguments or elements are expected. It also uses three dots ... before the iterable.

const fruits = ['apple', 'banana', 'orange'];
const vegetables = ['carrot', 'broccoli'];

const food = [...fruits, ...vegetables];
console.log(food); // Output: ['apple', 'banana', 'orange', 'carrot', 'broccoli']

9. Hoisting

Hoisting is a mechanism in JavaScript where variable and function declarations are moved to the top of their scope during the compilation phase. This allows you to use variables and functions before they are declared in your code.

Function declarations are hoisted with their definition, while variable declarations are hoisted with an initial value of undefined.

console.log(hoistedVar); // Output: undefined
console.log(hoistedFunc()); // Output: Hoisting works!

var hoistedVar = 'I am hoisted!';
function hoistedFunc() {
  return 'Hoisting works!';
}

10. Pure Functions

A pure function is a function that always produces the same output for the same input and has no side effects. It does not depend on or modify any external state.

function pureAdd(a, b) {
  return a + b;
}

console.log(pureAdd(3, 4)); // Output: 7
console.log(pureAdd(3, 4)); // Output: 7 (The output remains the same for the same input)

11. Call Stack

The call stack is a data structure that JavaScript uses to keep track of function calls and their execution context. When a function is called, it's added to the top of the call stack. Once the function has finished executing, it's removed from the call stack, and the execution continues with the next function in the stack.

function funcA() {
  console.log('Function A');
  funcB();
}

function funcB() {
  console.log('Function B');
}

console.log('Start');
funcA();
console.log('End');

// Output:
// Start
// Function A
// Function B
// End

6. Arrays

1. What is an Array?

An array is a collection of elements, each identified by an index. In JavaScript, arrays are dynamic in size, meaning they can grow or shrink as needed. You can think of an array as a list of items, where each item has a unique position (index).

// Creating an array
const fruits = ['apple', 'banana', 'cherry', 'date'];

2. Essential Array Methods

Let's explore some common and essential array methods in JavaScript:

1. map method

map is a method that creates a new array by applying a given function to every element of an existing array. Picture a conveyor belt where items go in, get transformed, and come out on the other side.

const numbers = [1, 2, 3, 4];
const doubledNumbers = numbers.map(number => number * 2);

console.log(doubledNumbers); // Output: [2, 4, 6, 8]

2. filter method

filter is used to create a new array containing only the elements that satisfy a given condition. Imagine a sieve that only allows specific items to pass through.

const peopleAges = [12, 23, 8, 30, 17];
const adults = peopleAges.filter(age => age >= 18);

console.log(adults); // Output: [23, 30]

3. reduce method

reduce is a method that takes an array and reduces it to a single value by applying a function to each element. It's like folding a piece of paper repeatedly until you have a single, final result.

const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);

console.log(sum); // Output: 10

4. forEach method

forEach is a method that executes a given function for each element in an array. It's like a loop that goes through each item one by one.

const fruits = ['apple', 'banana', 'cherry', 'date'];
fruits.forEach((fruit, index) => {
  console.log(`Fruit ${index + 1}: ${fruit}`);
});

// Output:
// Fruit 1: apple
// Fruit 2: banana
// Fruit 3: cherry
// Fruit 4: date

5. concat method

concat is a method that combines two or more arrays into a single array. Picture a row of train cars being connected to form a long train.

const array1 = [1, 2, 3];
const array2 = [4, 5, 6];
const combinedArray = array1.concat(array2);

console.log(combinedArray); // Output: [1, 2, 3, 4, 5, 6]

6. find method

find is a method that returns the first element in an array that satisfies a given condition. It's like searching for a needle in a haystack.

const numbers = [2, 7, 10, 15, 18];
const firstEvenNumber = numbers.find(number => number % 2 === 0);

console.log(firstEvenNumber); // Output: 2

7. indexOf method

indexOf is a method that returns the index of the first occurrence of a specified element in an array. If the element is not found, it returns -1. Picture searching for a specific page in a book and finding the page number.

const fruits = ['apple', 'banana', 'cherry', 'date'];
const indexOfCherry = fruits.indexOf('cherry');

console.log(indexOfCherry); // Output: 2

8. splice method

splice is a versatile method that can be used to add or remove elements from an array. It's like cutting a piece of cloth and inserting another piece or sewing the remaining parts together.

// The first parameter (1) specifies the start index, the second parameter (2) specifies the number of elements to remove, and the following parameters ('giraffe', 'zebra') are the elements to add.

const animals = ['dog', 'cat', 'elephant', 'lion'];
const removedAnimals = animals.splice(1, 2, 'giraffe', 'zebra');

console.log(animals); // Output: ['dog', 'giraffe', 'zebra', 'lion']
console.log(removedAnimals); // Output: ['cat', 'elephant']

9. slice method

slice is a method that returns a shallow copy of a portion of an array. Imagine cutting a slice of cake without altering the original cake.

const numbers = [1, 2, 3, 4, 5];
const slicedNumbers = numbers.slice(1, 4);

console.log(slicedNumbers); // Output: [2, 3, 4]
console.log(numbers); // Output: [1, 2, 3, 4, 5] (original array remains unchanged)

10. join method

join is a method that combines all elements of an array into a single string. It's like taking separate words and joining them to form a sentence.

// The parameter (' ') specifies the separator to be used between the elements in the resulting string.

const words = ['Hello', 'world', 'I', 'am', 'JavaScript'];
const sentence = words.join(' ');

console.log(sentence); // Output: 'Hello world I am JavaScript'

7. Objects

Imagine you have a filing cabinet where you store information about different things. Each drawer in the cabinet represents an object. Each drawer has different folders, and each folder contains specific information about the item. This is very similar to how objects work in JavaScript. Objects help us organize and store data in a structured manner, making our code tidy, just like a well-organized filing cabinet!

In JavaScript, objects are a collection of key-value pairs, where the key is a string and the value can be any data type (string, number, array, function, or another object). The keys are also known as properties or methods when they have function values.

const car = {
  make: 'Toyota',
  model: 'Corolla',
  year: 2020,
  startEngine: function() {
    console.log('Engine started');
  }
};

// Output: {make: "Toyota", model: "Corolla", year: 2020, startEngine: ƒ}
console.log(car);

1. Object Literals

An object literal is a concise way to create objects using a comma-separated list of key-value pairs wrapped in curly braces {}. It's like writing a shopping list for your favourite fruits.

Let's say we have three different fruits with their respective colours and tastes. We can represent this information as objects using object literals:

const batman = {
  secretIdentity: 'Bruce Wayne',
  city: 'Gotham'
};

const spiderman = {
  secretIdentity: 'Peter Parker',
  city: 'New York'
};

const wonderWoman = {
  secretIdentity: 'Diana Prince',
  city: 'Themyscira'
};

// Output: {secretIdentity: "Bruce Wayne", city: "Gotham"}
console.log(batman);

2. Accessing Properties

You can access an object's properties in two ways: dot notation and bracket notation. It's like opening a drawer and looking at a specific folder inside it, or finding the ice cream in your freezer (I know it's in there somewhere).

1. Dot Notation

Dot notation is the most common way to access properties. It's like opening a drawer and looking at a specific folder inside it.

// Output: "red"
console.log(apple.color);

2. Bracket Notation

Bracket notation allows you to access properties using a string or a variable that contains the property name. It's like looking for a folder in a drawer by reading its label.

// Output: "sweet"
console.log(apple['taste']);

const propertyName = 'taste';
// Output: "sweet"
console.log(apple[propertyName]);

3. Object Methods

JavaScript provides various built-in methods that help you work with objects, like a Swiss Army Knife for your code.

1. Object.keys()

Object.keys() returns an array containing the object's property names, just like reading the table of contents in a book.

// Output: ["color", "taste"]
console.log(Object.keys(apple));

2. Object.values()

Object.values() returns an array containing the object's property values, like getting a list of all the ingredients in a recipe.

// Output: ["red", "sweet"]
console.log(Object.values(apple));

3. Object.entries()

Object.entries() returns an array of key-value pairs for the object, like reading both the chapter titles and their page numbers in a book.

// Output: [["color", "red"], ["taste", "sweet"]]
console.log(Object.entries(apple));

4. Object.assign()

Object.assign() is used to copy the values of all enumerable properties from one or more source objects to a target object.

const newApple = Object.assign({}, apple, {color: 'green'});

// Output: {color: "green", taste: "sweet"}
console.log(newApple);

// The original apple object remains unchanged
// Output: {color: "red", taste: "sweet"}
console.log(apple);

4. Shorthand Property Names

Shorthand property names, also known as object property value shorthand, are a concise way to create new objects when the property names match the variable names. It allows you to create objects without explicitly specifying the key-value pairs.

const color = 'blue';
const type = 'sedan';

const car = {
  color,
  type
};

// Output: {color: "blue", type: "sedan"}
console.log(car);

5. Prototypes and Prototypal Inheritance

Prototypes in JavaScript are a mechanism for inheritance. They allow objects to share properties and methods. Every object in JavaScript has a prototype, which is an internal link to another object.

When you try to access a property or method on an object, JavaScript first looks at the object itself. If it cannot find the property, it checks the object's prototype. This process continues up the prototype chain until either the property is found or the chain ends.

Imagine a family tree where each person has their own set of skills, but they also inherit skills from their parents, grandparents, and so on. This is very similar to how prototypes work in JavaScript, except that our family tree is filled with objects instead of people!

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Person.prototype.getFullName = function() {
  return this.firstName + ' ' + this.lastName;
};

const person1 = new Person('John', 'Doe');
const person2 = new Person('Jane', 'Doe');

// Output: "John Doe"
console.log(person1.getFullName());

// Output: "Jane Doe"
console.log(person2.getFullName());

In the example above, the getFullName method is added to the prototype of the Person constructor function. As a result, all instances of Person (e.g., person1 and person2) can access and use the getFullName method through prototypal inheritance.


8. Classes and Inheritance

Think of a class as a blueprint for creating objects, like a recipe for baking your favourite superhero-themed cookies. It defines a set of properties and methods that the objects created from it will have, ensuring that every superhero cookie you bake will turn out perfect.

1. Class Declaration and Instantiation

Declaring a class in Javascript is like creating a blueprint for a house. The blueprint defines the structure of the house, such as the number of rooms, their dimensions, and the materials to be used. In the same way, a class declaration defines the structure and behaviour of objects created from it.

// Class declaration
class House {
  constructor(rooms, floors) {
    this.rooms = rooms;
    this.floors = floors;
  }

  getHouseInfo() {
    return `This house has ${this.rooms} rooms and ${this.floors} floors.`;
  }
}

// Instantiating a new House object
const myHouse = new House(4, 2);

// Output: This house has 4 rooms and 2 floors.
console.log(myHouse.getHouseInfo());

2. Constructors

A constructor is a special method inside a class that gets called when you create a new object. It's like a foreman who oversees the construction of a house while wearing a superhero cape. The constructor initializes the object's properties and sets up its initial state.

class House {
  // Constructor method
  constructor(rooms, floors) {
    this.rooms = rooms;
    this.floors = floors;
  }
}

3. Inheritance and extends

Inheritance allows you to create a new class that inherits properties and methods from an existing class. This promotes code reuse and modularity. In our house analogy, think of inheritance as creating a blueprint for a specific type of house, like a bungalow or a mansion, based on the general blueprint for a house.

// Base class
class House {
  constructor(rooms, floors) {
    this.rooms = rooms;
    this.floors = floors;
  }

  getHouseInfo() {
    return `This house has ${this.rooms} rooms and ${this.floors} floors.`;
  }
}

// Derived class
class Mansion extends House {
  constructor(rooms, floors, hasPool) {
    super(rooms, floors);
    this.hasPool = hasPool;
  }

  getMansionInfo() {
    return `${super.getHouseInfo()} It ${this.hasPool ? "has" : "does not have"} a pool.`;
  }
}

// Instantiate a new Mansion object
const myMansion = new Mansion(10, 4, true);

// Output: This house has 10 rooms and 4 floors. It has a pool.
console.log(myMansion.getMansionInfo());

4. super Keyword

The super keyword is used to call the constructor of a parent class, allowing you to reuse the parent class's initialization code. It's like taking the general blueprint for a house and adding specific details for a bungalow or a mansion.

class Mansion extends House {
  constructor(rooms, floors, hasPool) {
    super(rooms, floors); // Call the parent class's constructor
    this.hasPool = hasPool;
}
}

In the example above, we use super(rooms, floors) to call the constructor of the House class. This sets the rooms and floors properties in the Mansion object. Then, we add property, hasPool, specific to the Mansion class.

The super keyword can also be used to access parent class methods. In the getMansionInfo() method, we use super.getHouseInfo() to get the house information string from the parent class before adding details about the pool:

getMansionInfo() {
  return `${super.getHouseInfo()} It ${this.hasPool ? "has" : "does not have"} a pool.`;
}

5. this Keyword

The this keyword refers to the current instance of the class, allowing you to access its properties and methods. In our house analogy, it's like saying "this house" to refer to the specific house you are talking about.

class House {
  constructor(rooms, floors) {
    this.rooms = rooms; // 'this' refers to the current instance of the House class
    this.floors = floors;
  }
}

In the example above, we use this.rooms and this.floors to set the properties of the current instance of the House class.


9. Destructuring

Destructuring is a powerful syntax introduced in ES6, which allows you to extract properties from an object or elements from an array and assign them to variables in a concise and readable manner. Destructuring is like a magician pulling rabbits out of a hat but with variables and data structures instead of rabbits and hats!

Let's look at two forms of destructuring - Object Destructuring and Array Destructuring.

1. Object Destructuring

Think of object destructuring like opening a box and taking out its contents. Imagine you have a box with different items inside, each with a label. With object destructuring, you can easily take out the items you need using their labels without having to rummage through the entire box.

Here's a simple example:

const person = {
  name: "John Doe",
  age: 30,
  city: "New York"
};

// Destructuring syntax
const { name, age } = person;

console.log(name); // Output: John Doe
console.log(age); // Output: 30

In the example above, we have a person object with three properties. Using the destructuring syntax, we can extract the name and age properties into separate variables.

Best Practice When destructuring objects, it's a good idea to use the same variable names as the property names. This makes your code more readable and easier to understand.

2. Array Destructuring

Array destructuring is similar to object destructuring but works with arrays instead of objects. It's like opening a pack of cards and taking out cards based on their position in the pack.

Here's an example:

const fruits = ["apple", "banana", "cherry"];

// Array destructuring syntax
const [firstFruit, secondFruit] = fruits;

console.log(firstFruit); // Output: apple
console.log(secondFruit); // Output: banana

In this example, we have an array of fruits. Using the destructuring syntax, we can extract the first and second elements of the array into separate variables.

Best Practice When destructuring arrays, make sure to leave out unnecessary commas to skip the elements you don't need.

3. Optional Chaining (?.)

Optional chaining is a feature introduced in ES2020 that allows you to access deeply nested properties of an object without having to check for the existence of each property in the chain. Think of it as a "safe" way to navigate through a series of doors in a haunted house without worrying about whether the next door is locked or not, or if a spooky ghost is hiding behind it.

Here's an example:

const user = {
  name: "Jane Doe",
  address: {
    street: "123 Main St",
    city: "Los Angeles"
  }
};

// Traditional way
const city = user && user.address && user.address.city;

// Optional chaining syntax
const cityWithOptionalChaining = user?.address?.city;

console.log(city); // Output: Los Angeles
console.log(cityWithOptionalChaining); // Output: Los Angeles

In this example, we want to access the city property nested inside the address object. With optional chaining, we can safely access the city property without having to check if user and address exist.

Best Practice Use optional chaining when dealing with objects that may have nested properties that could be undefined or null. It helps to simplify your code and make it more readable by reducing the number of checks you need to perform.

4. Combining Destructuring and Optional Chaining

You can also combine destructuring and optional chaining for even more concise and readable code. Let's take a look at an example:

const data = {
  user: {
    name: "John Doe",
    address: {
      street: "123 Main St",
      city: "Los Angeles"
    }
  }
};

// Combine object destructuring and optional chaining
const { name, address } = data.user ?? {};

console.log(name); // Output: John Doe
console.log(address); // Output: { street: '123 Main St', city: 'Los Angeles' }

In this example, we destructure the name and address properties from the user object. If the user object doesn't exist, we provide a default empty object ({}) using the nullish coalescing operator (??). This way, we can safely destructure properties without worrying about undefined or null values.

Best Practice When combining destructuring and optional chaining, use the nullish coalescing operator (??) to provide default values for your variables. This helps to prevent errors and ensures that your code is more resilient to unexpected data structures.


10. Template Literals

Imagine you're trying to build a sandwich by combining different ingredients. In JavaScript, this process is called string concatenation. Before ES6, we had to use the + operator to concatenate strings, which often led to messy and hard-to-read code.

ES6 introduced template literals that make it easier to create strings with dynamic content. Template literals use backticks instead of quotes and allow for string interpolation using placeholders wrapped in ${}.

1. Using Backticks and Placeholders for String Interpolation

Let's understand this with an example. Suppose we want to create a personalized greeting message for users:

const user = {
  firstName: "John",
  lastName: "Doe",
  age: 30,
};

// Using string concatenation (old way)
const greeting = "Hello, " + user.firstName + " " + user.lastName + "! You are " + user.age + " years old.";

// Using template literals (new way)
const greetingWithTemplate = `Hello, ${user.firstName} ${user.lastName}! You are ${user.age} years old.`;

console.log(greeting); // Output: Hello, John Doe! You are 30 years old.
console.log(greetingWithTemplate); // Output: Hello, John Doe! You are 30 years old.

As you can see, template literals make the code cleaner and easier to read.

2. Higher-Order Functions and Closures

Higher-order functions and closures are powerful concepts in JavaScript that enable functional programming and provide better code organization.

1. Functions as Arguments

Higher-order functions are functions that either take other functions as arguments or return them as results.

Imagine you're at a restaurant, and you want a dish made with specific ingredients. The chef (higher-order function) takes your choice of ingredients (functions as arguments) and prepares the dish for you.

This concept is like delegating tasks to others. Let's use a meaningful example to illustrate this:

// A simple add and multiply function
function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

// A higher-order function that takes a function as an argument
function calculate(operation, a, b) {
  return operation(a, b);
}

console.log(calculate(add, 5, 3)); // Output: 8
console.log(calculate(multiply, 5, 3)); // Output: 15

In the example above, calculate is a higher-order function that takes another function (add or multiply) as an argument.

2. Functions as Return Values

Higher-order functions can also return functions as results.

Continuing with the restaurant example, imagine the chef (higher-order function) can also create customized recipes (functions as return values) based on the ingredients you choose.

Here's an example:

function createMultiplier(multiplier) {
  return function (number) {
    return number * multiplier;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // Output: 10
console.log(triple(5)); // Output: 15

In the example above, createMultiplier is a higher-order function that returns a new function with a specific multiplier value.

3. Understanding Closures and Their Use Cases

A closure is a function that has access to its outer function's scope even after the outer function has executed. In simpler terms, closures remember their environment. They're like a backpack that a function carries with it, containing all the variables and values from its surrounding scope.

Here's an example to illustrate closures:

function createGreeting(greeting) {
  return function (name) {
    return `${greeting}, ${name}!`;
  };
}

const greetInEnglish = createGreeting("Hello");
const greetInSpanish = createGreeting("Hola");

console.log(greetInEnglish("John")); // Output: Hello, John!
console.log(greetInSpanish("John")); // Output: Hola, John!

In the example above, createGreeting returns a new function that has access to the greeting variable from its outer scope. This new function is a closure.

Closures are useful in various use cases, such as:

  1. Encapsulation: Closures allow you to create private variables and methods that are only accessible within the closure, thus providing a level of data protection.

  2. Event handlers: Closures enable you to reference variables from the outer scope in event handlers, which is useful for maintaining state between multiple events.

  3. Function factories: As demonstrated earlier, closures can be used to create functions with specific behaviour based on the values from the outer scope.


11. Event Handling and DOM Manipulation

1. Adding Event Listeners

Event listeners are used to detect user interactions, like clicking a button, and respond to them. To add an event listener, you can use the addEventListener() method.

Event listeners are like the ears of your JavaScript code. They're constantly listening for user interactions (like button clicks) so your code can respond accordingly.

Imagine you have a button, and you want to display an alert when the button is clicked. You can do this by adding a click event listener to the button.

<!-- HTML -->
<button id="myButton">Click me!</button>
// JavaScript
const button = document.getElementById('myButton');

button.addEventListener('click', () => {
  alert('Button clicked!');
});

In this example, the addEventListener() method takes two arguments: the event type (in this case, 'click') and the callback function that should be executed when the event occurs.

2. Removing Event Listeners

Sometimes, you may want to remove an event listener after it's been added. You can do this using the removeEventListener() method.

To remove an event listener, you need to pass the same event type and a callback function as you did when adding the listener. To make sure you're using the same callback function, you can store it in a variable.

const button = document.getElementById('myButton');

function handleClick() {
  alert('Button clicked!');
  button.removeEventListener('click', handleClick); // Remove the event listener after the first click
}

button.addEventListener('click', handleClick);

In this example, the handleClick function will only be executed once because the event listener is removed after the first click.

3. Event Propagation: Capturing and Bubbling

Event propagation is like a game of telephone in the DOM. When an event occurs, it gets passed along from one element to another in a specific order. Understanding this order is crucial for managing event listeners and creating interactive web pages.

There are two phases of event propagation: capturing and bubbling.

  1. Capturing phase: Events are first captured by the outermost element and then propagate down through the DOM tree to the target element.

  2. Bubbling phase: Events then bubble up from the target element through the DOM tree to the outermost element.

By default, event listeners are added in the bubbling phase. However, you can also add event listeners in the capturing phase by passing a third argument, true, to the addEventListener() method.

const outerDiv = document.getElementById('outerDiv');
const innerDiv = document.getElementById('innerDiv');

outerDiv.addEventListener('click', () => {
  console.log('Capturing: outerDiv');
}, true); // Capture phase

innerDiv.addEventListener('click', () => {
  console.log('Bubbling: innerDiv');
}, false); // Bubble phase (default)

In this example, if you click the inner div, the console will display:

Capturing: outerDiv
Bubbling: innerDiv

4. Selecting Elements

Before you can manipulate the DOM, you need to know how to find and select the elements you want to work with. It's like trying to find your friend in a crowded room – you need some way to identify them.

There are several ways to do this:

  • querySelector(): Selects the first element that matches the specified CSS selector.

  • getElementById(): Selects an element by its ID.

  • getElementsByClassName(): Select elements by their class name.

  • getElementsByTagName(): Select elements by their tag name.

Here are some examples of how to use these methods:

<!-- HTML -->
<div id="myDiv">I'm a div!</div>
<p class="myClass">I'm a paragraph with class "myClass"!</p>
const div = document.querySelector('#myDiv'); // Selects the div with the ID "myDiv"
const divById = document.getElementById('myDiv'); // Selects the div with the ID "myDiv"
const paragraphs = document.getElementsByClassName('myClass'); // Selects all paragraphs with the class "myClass"
const allParagraphs = document.getElementsByTagName('p'); // Selects all paragraph elements

5. Modifying Elements

Once you've selected elements, you can modify them in various ways, such as changing their content or attributes. Here are some useful methods for modifying elements:

  • innerHTML: Sets or returns the HTML content of an element.

  • textContent: Sets or returns the text content of an element.

  • setAttribute(): Sets the value of an attribute on the element.

  • removeAttribute(): Removes an attribute from the element.

  • classList: Returns an object representing the element's class attributes.

Here are some examples of how to use these methods:

const div = document.getElementById('myDiv');

div.innerHTML = '<p>I just changed the content of this div!</p>'; // Changes the HTML content of the div
div.textContent = 'Now it\'s just text!'; // Changes the text content of the div
div.setAttribute('data-custom-attribute', 'myValue'); // Adds a custom attribute to the div
div.removeAttribute('data-custom-attribute'); // Removes the custom attribute from the div
div.classList.add('newClass'); // Adds a new class to the div
div.classList.remove('newClass'); // Removes the class from the div

6. Creating, Appending, and Removing Elements

You can also create new elements, append them to existing elements, and remove elements from the DOM. Here are some useful methods for these tasks:

createElement(): Creates a new element with the specified tag name. appendChild(): Appends a new child element to the current element. removeChild(): Removes a child element from the current element.

Here's an example of how to use these methods:

<!-- HTML -->
<div id="myDiv"></div>
// JavaScript
const div = document.getElementById('myDiv');
const newParagraph = document.createElement('p'); // Creates a new paragraph element

newParagraph.textContent = 'I am a new paragraph!'; // Sets the text content of the new paragraph
div.appendChild(newParagraph); // Appends the new paragraph to the div

// To remove the new paragraph, you can use the following code:
div.removeChild(newParagraph); // Removes the new paragraph from the div

12. Asynchronous Programming

Imagine a world where you must wait in line for everything, even for tasks that don't require your immediate attention. Sounds frustrating, right? Well, that's where asynchronous programming comes to the rescue!

Asynchronous programming is a way of handling tasks that take time to complete, such as fetching data from a server, reading a file, or performing complex calculations. In this article, we'll help you understand asynchronous code in JavaScript by using great analogies, meaningful examples, and best practices.

1. Understanding Asynchronous Code

Imagine you are at a fast-food restaurant, and you place an order for a meal. Instead of waiting at the counter for your meal, you are given a token and asked to wait. Meanwhile, other customers can place their orders, and the restaurant can serve multiple customers simultaneously. This is similar to how asynchronous code works in JavaScript. It allows your program to continue executing other tasks while waiting for a time-consuming task to complete.

JavaScript is single-threaded, which means it executes one task at a time. However, using asynchronous code, you can create the illusion of multitasking by handling tasks in the background while other code continues to run.

2. Callback Functions

A callback function is a function passed as an argument to another function, which will be executed later when a specific event occurs. In the fast-food analogy, the callback function is the action taken when your token number is called, and your meal is ready.

Here's a simple example of using a callback function to simulate a time-consuming task using setTimeout():

function orderPizza(callback) {
  // Simulate the time it takes to prepare and deliver a pizza
  setTimeout(function() {
    const pizza = 'Your pizza is ready!';
    callback(pizza);
  }, 3000);
}

function enjoyPizza(pizza) {
  console.log(pizza); // "Your pizza is ready!"
}

// Order a pizza and pass the enjoyPizza function as a callback
orderPizza(enjoyPizza);
console.log('You can watch TV while waiting...'); // "You can watch TV while waiting..."

In this example, setTimeout() is used to simulate a time-consuming task. The displayResult function is passed as a callback, and it will be executed after the task is completed.

3. Creating and Using Promises

Promises are a more modern approach to handle asynchronous code in JavaScript. A promise represents the eventual result of an asynchronous operation. It can be in one of three states:

  • Pending: The initial state; neither fulfilled nor rejected.

  • Fulfilled: The operation was completed successfully, and the promise has a resulting value.

  • Rejected: The operation failed, and the promise has a reason for the failure.

Continuing with the fast-food analogy, a promise is like the token you receive when placing an order. The token represents the eventual completion of your order, and its state can be fulfilled (you receive your meal) or rejected (your order was cancelled).

Here's an example of creating and using a promise:

function orderPizza() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const pizza = 'Your pizza is ready!';
      resolve(pizza);
    }, 3000);
  });
}

orderPizza()
  .then(pizza => {
    console.log(pizza); // "Your pizza is ready!"
  })
  .catch(error => {
    console.error('An error occurred:', error);
  });

console.log('You can watch TV while waiting...'); // "You can watch TV while waiting..."

In this example, the timeConsumingTask function returns a promise, which is then handled using the then and catch methods.

4. Async and Await

async and await are modern syntactic sugar for working with promises. An async function always returns a promise, and you can use the await keyword to wait for a promise to be resolved or rejected.

Using the fast-food analogy again, the async and await keywords are like a more organized way to handle the token system in the restaurant. You can clearly see the flow of actions, making it easier to follow.

Here's an example of using async and await:


function orderPizza() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const pizza = 'Your pizza is ready!';
      resolve(pizza);
    }, 3000);
  });
}


async function main() {
  try {
    console.log('You can watch TV while waiting...'); // "You can watch TV while waiting..."
    const pizza = await orderPizza();
    console.log(pizza); // "Your pizza is ready!"
  } catch (error) {
    console.error('An error occurred:', error);
  }
}

main();

In this example, we define an async function main, which is responsible for handling the result of the timeConsumingTask. We use the await keyword to wait for the promise to resolve before executing the subsequent code.

5. Error Handling: try...catch and finally

When working with asynchronous code, errors can occur, and it's essential to handle them properly. The try...catch statement allows you to handle exceptions gracefully without breaking the program flow.

In our fast-food analogy, error handling is like the staff informing you that your order was cancelled and providing an explanation (e.g., an ingredient shortage). The staff handles this issue without disrupting other customers' orders.

Here's an example of using try...catch with async/await:

async function timeConsumingTask() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const shouldFail = Math.random() < 0.5;
      if (shouldFail) {
        reject(new Error('Task failed!'));
      } else {
        resolve('Task completed!');
      }
    }, 3000);
  });
}

async function main() {
  try {
    console.log('Other tasks continue...'); // "Other tasks continue..."
    const result = await timeConsumingTask();
    console.log(result); // "Task completed!" or error caught
  } catch (error) {
    console.error('An error occurred:', error.message); // "An error occurred: Task failed!"
  } finally {
    console.log('Task attempted.'); // "Task attempted."
  }
}

main();

In this example, we introduce a random chance of the timeConsumingTask failing. If it fails, we reject the promise with an error. In the main function, we use try...catch to handle errors and finally to execute code regardless of whether an error occurred or not.


13. Modules

Remember playing with LEGO blocks? You could create fantastic structures by assembling small, reusable pieces. Well, programming with modules is a lot like playing with LEGO! In programming, we use modules to split our code into smaller, reusable parts.

1. ES6 Module Syntax: Import and Export

ECMAScript 6 (ES6) introduced a new module syntax that makes it easier to manage and organize our code. Let's take a look at how we can use import and export to work with modules.

// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// main.js
import { add, subtract } from "./math.js";

console.log(add(3, 5)); // Output: 8
console.log(subtract(10, 4)); // Output: 6

In the example above, we have two files: math.js and main.js. We define two functions add and subtract in math.js and export them using the export keyword. Then, in main.js, we use the import keyword to import these functions and use them.

2. Default and Named Exports

There are two types of exports in ES6 modules: default and named exports. Let's see how they differ.

1. Named Exports:

// math.js
export function add(a, b) {
  return a + b;
}

// main.js
import { add } from "./math.js";

console.log(add(3, 5)); // Output: 8

In this example, we used a named export for the add function. This allows us to export multiple named functions or variables from a single module.

2. Default Exports:

// math.js
export default function add(a, b) {
  return a + b;
}

// main.js
import add from "./math.js";

console.log(add(3, 5)); // Output: 8

In this case, we used a default export for the add function. Default exports are useful when a module has a single main function or variable that it exports.


14. Immutability

Immutability is a concept where an object's state cannot be modified after it's created. Think of it as a contract that once signed, cannot be changed. Immutability is essential in JavaScript because it helps prevent bugs, simplify code, and improve performance.

Think of immutability as a super-strict contract that once signed, cannot be changed. Not even by a cute puppy with the most adorable puppy eyes.

1. Importance of Immutability in JavaScript

  • Prevents bugs: Since the state of an immutable object cannot be changed, it becomes easier to reason about the code and avoid unexpected side effects.

  • Simplifies code: Immutability enforces a functional programming style, which can lead to cleaner and more maintainable code.

  • Improves performance: By using immutable data structures, JavaScript engines can optimize memory usage and perform faster comparisons.

2. Techniques to Maintain Immutability

Here are some techniques to maintain immutability in JavaScript:

  1. Use const instead of let or var: By using const, you ensure that a variable cannot be reassigned.
const PI = 3.14159;
PI =3.14; // Error: Assignment to constant variable.
  1. Use Object.freeze(): This method prevents modifications to an object, ensuring that its properties cannot be added, deleted, or changed.
const person = Object.freeze({ name: 'John', age: 30 });
person.age = 31; // Error: Cannot assign to read-only property 'age' of object
  1. Use immutable data structures: Use libraries like Immutable.js that provide immutable data structures like List, Map, and Set.

  2. Return new objects instead of modifying existing ones: When updating an object, create a new object with the updated properties instead of modifying the original object.

const updatePersonAge = (person, newAge) => {
  return { ...person, age: newAge };
};

const person = { name: 'John', age: 30 };
const updatedPerson = updatePersonAge(person, 31);
console.log(updatedPerson); // Output: { name: 'John', age: 31 }
console.log(person); // Output: { name: 'John', age: 30 }

15. Regular Expressions

Regular expressions, often shortened to regex or regexp, are a powerful way to work with strings in JavaScript. They provide a concise and flexible means for identifying and manipulating string patterns, allowing you to search, replace, and validate strings in a performant manner.

Imagine a locksmith who has a magical key that can unlock any door with a specific pattern. In the world of JavaScript, regular expressions are that magical key, and the doors are the strings we work with.

Example Let's consider a scenario where we need to find all the email addresses in a given text.

const text = "Contact us at support@example.com or john.doe@example.org.";

// Create a regular expression pattern for email addresses
const emailPattern = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;

// Search for email addresses in the text
const emails = text.match(emailPattern);

console.log(emails); // ["support@example.com", "john.doe@example.org"]

If you're looking to improve your string manipulation skills in JavaScript, learning about regular expressions is a great place to start. With regex, you can easily search for, replace, and validate string patterns. To get started, I recommend checking out regexlearn.com.


16. Miscellaneous Topics

1. Execution Context

An execution context is an environment in which JavaScript code is executed. Each function invocation creates a new execution context, with its own set of variables, functions, and arguments.

Consider a chef working in a kitchen. The kitchen is the execution context, where the chef (JavaScript engine) prepares meals (executes code). Each time the chef starts cooking a new dish (function), they create a new workstation (execution context) with its own ingredients (variables) and tools (functions).

2. Scope Chain

A scope chain is a hierarchical structure of nested execution contexts that determines the visibility of variables and functions.

Imagine a multi-story building, where each floor represents an execution context. The scope chain is like an elevator that moves up and down the building, allowing access to the variables and functions on each floor. The elevator can access the floors above, but not the floors below.

3. Lexical Environment

A lexical environment is the data structure that holds the mapping between variable/function names and their corresponding values within a specific scope.

The lexical environment is like a virtual treasure chest in a video game. Each level (execution context) has its own treasure chest (lexical environment), containing shiny loot (variables and functions) specific to that level.

// Global execution context
const globalVar = "Global variable";

function outerFunc() {
  // Outer function execution context
  const outerVar = "Outer variable";

  function innerFunc() {
    // Inner function execution context
    const innerVar = "Inner variable";

    console.log(globalVar); // "Global variable"
    console.log(outerVar); // "Outer variable"
    console.log(innerVar); // "Inner variable"
}

innerFunc();
}

outerFunc();

4. use strict Directive

The 'use strict' directive is like a strict school teacher that enforces discipline in your JavaScript code. It helps you catch common coding mistakes and "unsafe" actions, such as using undeclared variables or assigning a value to a read-only property. You might not always like it, but it's definitely for your own good!

Example

"use strict";

function strictFunc() {
  undeclaredVar = "Undeclared variable"; // Throws a ReferenceError
}

strictFunc();

5. Collections and Data Structures: Set()

Think of a Set as a super-exclusive club for JavaScript values. This club only allows unique values of any type to enter, whether primitive or object references. It's the perfect place to eliminate duplicates and maintain a collection of distinct elements.

Example

const numbers = [1, 2, 3, 4, 4, 5, 6, 6, 7, 8, 9];

// Create a Set from the numbers array
const uniqueNumbers = new Set(numbers);

console.log(uniqueNumbers); // Set { 1, 2, 3, 4, 5, 6, 7, 8, 9 }

Alright, folks, that brings us to the end of our whirlwind tour of the 18 must-know JavaScript topics! You've got the skills to become a web development wizard, and I'm excited to see what kind of magic you'll create with your newfound knowledge.

But let me tell you, the journey doesn't end here. The world of web development is vast and constantly evolving, and there's always something new to learn. So don't get too comfortable just yet - keep pushing yourself, stay curious, and always be open to new ideas and techniques.

And hey, if you're looking to connect with other developers and stay up-to-date on the latest trends and best practices, I'd love to connect with you on LinkedIn. Let's keep the conversation going and continue to grow and learn together. Cheers!