Sitemap

Javascript refresher

68 min readMar 12, 2025

Table of Contents

Chapter 1: Introduction to JavaScript

Chapter 2: Core Concepts: Variables, Data Types, and Operators

Chapter 3: Control Flow: Conditionals and Loops

Chapter 4: Functions and Scope

Chapter 5: Objects data structure

Chapter 6: Arrays

Chapter 7: Map and Set data structure

Chapter 8: Error Handling and Debugging

Chapter 9: String

Chapter 10: Object-Oriented Programming (OOP) in JavaScript

Chapter 11: Asynchronous JavaScript: Callbacks, Promises, and Async/Await

Chapter 12: Date

Chapter 1: Introduction to JavaScript

1.1 What is JavaScript? (History, Evolution)

1.1.1 Historical Context

  • JavaScript was created by Brendan Eich at Netscape in 1995.
  • Initially named “Mocha,” then “LiveScript,” and finally “JavaScript.”
  • Designed to add interactivity to web pages.
  • Its initial popularity was due to its integration within Netscape Navigator.
  • It has no relationship with Java though it might sound.

1.1.2 Evolution and Standardization

  • ECMAScript (ES) standardization by ECMA International.
  • ES1 (1997), ES2 (1998), ES3 (1999) — Early versions focused on basic functionality.
  • ES4 (abandoned) — A more ambitious version that was ultimately scrapped.
  • ES5 (2009) — Introduced important features like strict mode and JSON support.
  • ES6 (ES2015) — A major update with classes, modules, arrow functions, let/const, and more.
  • ES7+ (ES2016+) — Annual releases with incremental improvements.

1.1.3 JavaScript’s Role Today

  • Client-side web development (making web pages interactive).
  • Server-side development (Node.js).
  • Mobile app development (React Native, Ionic).
  • Desktop app development (Electron).
  • Game development (Phaser.js, Three.js).
  • Internet of Things (IoT) applications.

1.2 JavaScript’s Role in Web and Server Development

2.1 Client-Side (Browser):

<!DOCTYPE html>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>This is a simple HTML page.</p>
<button id="myButton">Click me</button>
</body>
</html>
  • Manipulating the DOM (Document Object Model).
  • Handling user events (clicks, form submissions).
  • Making AJAX requests (fetching data from servers).
  • Creating interactive user interfaces.
  • Example:
document.getElementById("myButton").addEventListener("click", function() {
alert("Button clicked!");
});

1.2.2 Server-Side (Node.js):

  • Building web servers and APIs.
  • Handling database interactions.
  • Creating real-time applications (WebSockets).
  • Performing file system operations.
  • Example:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello, Node.js!');
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});

1.3 Setting Up Your Development Environment

1.3.1 Browsers:

  • Modern browsers (Chrome, Firefox, Edge, Safari) have built-in JavaScript engines.
  • Developer tools (console, debugger, network tab) are essential.
  • Try to open the console in each browser.

1.3.2 Node.js:

  • Download and install Node.js from the official website (nodejs.org).
  • Verify installation using node -v and npm -v in the terminal.
  • Understanding npm (Node Package Manager).

1.3.3 Code Editors:

  • Visual Studio Code (VS Code) — Popular and versatile (free).
  • Sublime Text — Lightweight and fast (free).
  • Atom — Customizable and open-source (free).
  • WebStorm — Powerful IDE for web development (paid).
  • Setting up extensions for javascript development.

1.4 Hello, World! (Basic Syntax, Console Output)

1.4.1 Browser Example:

  • Create an HTML file (index.html).
  • Embed JavaScript code within <script> tags.
  • Example:
<!DOCTYPE html>
<html>
<head>
<title>Hello, World!</title>
</head>
<body>
<script>
console.log("Hello, World!");
</script>
</body>
</html>

1.4.2 Node.js Example:

  • Create a JavaScript file (hello.js).
  • Use console.log() to output to the terminal.
  • Example:
console.log("Hello, World! from Node.js");
  • Run the file using node hello.js in the terminal.

1.4.3 Basic Syntax Elements:

  • Statements and expressions.
  • Semicolons (optional but recommended).
  • Comments (// for single-line, /* ... */ for multi-line).
  • Case sensitivity.

Chapter 2: Core Concepts: Variables, Data Types, and Operators

2.1. Data Types

JavaScript is a dynamically-typed language, meaning you don’t need to explicitly declare the data type of a variable. The JavaScript engine infers the type based on the value assigned to it.

JavaScript has two main categories of data types based on how it is stored in memory as shown above: primitive and object.

2.1.1 Primitive Data Types: These are immutable (cannot be changed directly) and represent simple values.

  • Number: Represents numeric values, including integers and floating-point numbers.
let age = 30;
let price = 99.99;
let infinity = Infinity; // Represents positive infinity
let negativeInfinity = -Infinity; //Represents negative infinity
let notANumber = NaN; //Represents "Not a Number" (e.g., result of dividing by zero)
console.log(age, price, infinity, negativeInfinity, notANumber);
  • String: Represents textual data enclosed in single quotes (‘…’), double quotes (“…”), or backticks (template literals — …).
let name = "Alice";
let message = 'Hello, world!';
let templateString = `My name is ${name}`; // Using template literals (ES6) for string interpolation
console.log(name, message, templateString);
  • Boolean: Represents a logical value: true or false.
let isAdult = true;
let isLoggedIn = false;
console.log(isAdult, isLoggedIn);
  • Null: Represents the intentional absence of a value. It is an assignment value. null means that a variable has been explicitly set to nothing.
let user = null; // User is currently unknown or not available
console.log(user);
  • Undefined: Represents a variable that has been declared but has not been assigned a value. It essentially means the value doesn’t exist.
let city; // city is declared but not assigned a value
console.log(city);
  • Symbol (ES6): Represents a unique and immutable identifier. Symbols are often used as object property keys to avoid naming collisions.
const mySymbol = Symbol("description"); // description is optional
const anotherSymbol = Symbol("description");

console.log(mySymbol === anotherSymbol); // Output: false (Symbols are always unique)

const person = {
[mySymbol]: "Secret value" // Using Symbol as a property key
};

console.log(person[mySymbol]); // Output: Secret value
  • BigInt (ES2020): Represents integers of arbitrary precision (larger than the Number type can safely represent).
const bigNumber = 9007199254740991n; // Add "n" to the end to create a BigInt
const anotherBigNumber = BigInt(9007199254740995);

console.log(bigNumber + 1n); // Output: 9007199254740992n (BigInt addition)
console.log(anotherBigNumber); //Output: 9007199254740995n

2. 1.2 Object Data Type: Represents a collection of key-value pairs (properties) or a more complex entity. Objects are mutable (can be changed after creation).

  • Object: The most fundamental object type. Arrays and functions are also objects in JavaScript.
const person = {
name: "John",
age: 35,
city: "New York"
};

console.log(person.name); // Accessing properties using dot notation
console.log(person["age"]); // Accessing properties using bracket notation
  • Array: An ordered list of values.
const numbers = [1, 2, 3, 4, 5];
const fruits = ["apple", "banana", "orange"];

console.log(numbers[0]); // Accessing elements by index
console.log(fruits.length); // Getting the length of the array
  • Function: A reusable block of code (covered in detail in Chapter 4).
function greet(name) {
return "Hello, " + name + "!";
}

console.log(greet("World")); // Calling the function

2.2. Variables

Variables are used to store data in JavaScript. You can think of them as named containers that hold values.

  1. var vs. let vs. const (Scope and Hoisting)

var:

  • Function-scoped (or globally scoped if declared outside any function).
  • Hoisted to the top of its scope, but initialized with undefined. This means you can use the variable before it’s declared, but its value will be undefined.
  • Can be redeclared and reassigned within its scope.
function exampleVar() {
console.log(x); // Output: undefined (hoisting)
var x = 10;
console.log(x); // Output: 10
var x = 20; // Redeclaration is allowed
x = 30; // Reassignment is allowed
console.log(x); // Output: 30
}

exampleVar();

if (true) {
var globalVar = "I'm a var in a block";
}
console.log(globalVar); // Accessible outside the block (because `var` is function-scoped, not block-scoped)

let:

  • Block-scoped.
  • Hoisted, but not initialized. This means you cannot access the variable before its declaration; doing so will result in a ReferenceError (the “temporal dead zone”).
  • Can be reassigned within its scope but cannot be redeclared.
function exampleLet() {
// console.log(y); // Error: Cannot access 'y' before initialization (temporal dead zone)
let y = 10;
console.log(y); // Output: 10
// let y = 20; // Error: Identifier 'y' has already been declared
y = 30; // Reassignment is allowed
console.log(y); // Output: 30
}

exampleLet();

if (true) {
let blockLet = "I'm a let in a block";
}
// console.log(blockLet); // Error: blockLet is not defined outside the block (because `let` is block-scoped)

const:

  • Block-scoped.
  • Hoisted, but not initialized (same as let).
  • Cannot be redeclared or reassigned after initialization. However, if const is used with an object or array, the contents of the object or array can still be modified, just not the variable itself.
function exampleConst() {
// console.log(z); // Error: Cannot access 'z' before initialization (temporal dead zone)
const z = 10;
console.log(z); // Output: 10
// const z = 20; // Error: Identifier 'z' has already been declared
// z = 30; // Error: Assignment to constant variable

const myObject = { value: 1 };
myObject.value = 2; // Allowed (modifying the object's property)
console.log(myObject.value); // Output: 2
// myObject = { value: 3 }; // Error: Assignment to constant variable

const myArray = [1, 2, 3];
myArray.push(4); // Allowed (modifying the array's content)
console.log(myArray); // Output: [1, 2, 3, 4]
// myArray = [4,5,6]; //Error : Assignment to a constant variable
}

exampleConst();
if (true) {
const blockConst = "I'm a const in a block";
}
// console.log(blockConst); // Error: blockConst is not defined outside the block (because `const` is block-scoped)

2. Best Practices:

  • Always use const by default for variables that should not be reassigned. This helps prevent accidental errors.
  • Use let for variables that need to be reassigned.
  • Avoid using var in modern JavaScript code, as its function scope can lead to unexpected behavior. let and const offer better control and predictability with block scope.

3. Variable Naming Conventions:

  • Variable names must start with a letter, underscore (_), or dollar sign ($).
  • Subsequent characters can be letters, numbers, underscores, or dollar signs.
  • Variable names are case-sensitive (myVariable is different from myvariable).
  • Use descriptive and meaningful names.
  • Follow a consistent naming convention (e.g., camelCase: myVariableName).
let firstName = "David";
const PI = 3.14159; // Use all caps for constants
let _privateVariable = "This is intended to be private (convention)";

2.3. Operators

Operators are symbols that perform operations on one or more operands (values).

  1. Arithmetic Operators: Perform mathematical calculations.
  • + (Addition)
  • - (Subtraction)
  • * (Multiplication)
  • / (Division)
  • % (Modulo — remainder after division)
  • ** (Exponentiation — ES2016)
let a = 10;
let b = 5;

console.log(a + b); // Output: 15
console.log(a - b); // Output: 5
console.log(a * b); // Output: 50
console.log(a / b); // Output: 2
console.log(a % b); // Output: 0 (10 divided by 5 has no remainder)
console.log(a % 3); // Output: 1 (10 divided by 3 has remainder 1)
console.log(a ** b); // Output: 100000 (10 to the power of 5)

2. Assignment Operators: Assign values to variables.

  • = (Assignment)
  • += (Add and assign)
  • -= (Subtract and assign)
  • *= (Multiply and assign)
  • /= (Divide and assign)
  • %= (Modulo and assign)
  • **= (Exponentiation and assign)
let x = 10;

x = 20; // Assignment
console.log(x); // Output: 20

x += 5; // Add and assign (x = x + 5)
console.log(x); // Output: 25

x -= 2; // Subtract and assign (x = x - 2)
console.log(x); // Output: 23

x *= 3; // Multiply and assign (x = x * 3)
console.log(x); // Output: 69

x /= 2; // Divide and assign (x = x / 2)
console.log(x); // Output: 34.5

x %= 5; // Modulo and assign (x = x % 5)
console.log(x); // Output: 4.5

x **= 2; // Exponentiation and assign (x = x ** 2)
console.log(x); // Output: 20.25

3. Comparison Operators: Compare two values and return a boolean (true or false).

  • == (Equal to — loose equality)
  • === (Strictly equal to — strict equality)
  • != (Not equal to — loose inequality)
  • !== (Strictly not equal to — strict inequality)
  • > (Greater than)
  • < (Less than)
  • >= (Greater than or equal to)
  • <= (Less than or equal to)
let num1 = 10;
let num2 = "10";

console.log(num1 == num2); // Output: true (loose equality - type coercion)
console.log(num1 === num2); // Output: false (strict equality - no type coercion)
console.log(num1 != num2); // Output: false (loose inequality)
console.log(num1 !== num2); // Output: true (strict inequality)
console.log(num1 > 5); // Output: true
console.log(num1 < 15); // Output: true
console.log(num1 >= 10); // Output: true
console.log(num1 <= 10); // Output: true

Important: == vs. ===

  • == (loose equality): Compares values after type coercion (JavaScript tries to convert the values to a common type). This can lead to unexpected results.
  • === (strict equality): Compares values without type coercion. The values must be of the same type and have the same value to be considered equal.

Always use === and !== for comparisons unless you have a specific reason to use == or !=.

3. Logical Operators: Combine or modify boolean expressions.

  • && (Logical AND) — Returns true if both operands are true.
  • || (Logical OR) — Returns true if at least one operand is true.
  • ! (Logical NOT) — Returns the opposite of the operand’s boolean value.
let sunny = true;
let warm = true;

console.log(sunny && warm); // Output: true (both are true)
console.log(sunny || false); // Output: true (at least one is true)
console.log(!sunny); // Output: false (negation of true)

4. Bitwise Operators (Optional for Beginners): Perform operations on the binary representation of numbers. These are generally used in more advanced scenarios, such as low-level programming or working with binary data.

  • & (Bitwise AND)
  • | (Bitwise OR)
  • ^ (Bitwise XOR)
  • ~ (Bitwise NOT)
  • << (Left shift)
  • >> (Right shift)
  • >>> (Unsigned right shift)
let num3 = 5;  // Binary: 0101
let num4 = 3; // Binary: 0011

console.log(num3 & num4); // Output: 1 (Binary: 0001)
console.log(num3 | num4); // Output: 7 (Binary: 0111)

5. Type Operators:

  • typeof: Returns a string indicating the data type of a value.
console.log(typeof 10);        // Output: "number"
console.log(typeof "Hello"); // Output: "string"
console.log(typeof true); // Output: "boolean"
console.log(typeof null); // Output: "object" (historical quirk in JavaScript)
console.log(typeof undefined); // Output: "undefined"
console.log(typeof { name: "Alice" }); // Output: "object"
console.log(typeof [1, 2, 3]); // Output: "object"
console.log(typeof function() {}); // Output: "function"
console.log(typeof Symbol()); // Output: "symbol"
console.log(typeof 123n); // Output: "bigint"
  • instanceof: Checks if an object is an instance of a particular constructor function (class).
class Person {
constructor(name) {
this.name = name;
}
}

const person1 = new Person("Bob");
const arr = [1,2,3];

console.log(person1 instanceof Person); // Output: true
console.log(arr instanceof Array); //Output: true
console.log(arr instanceof Object); //Output: true (Arrays are also objects)
  • Operator Precedence: Determines the order in which operators are evaluated in an expression. Operators with higher precedence are evaluated before operators with lower precedence. You can use parentheses () to override the default precedence.
let result = 2 + 3 * 4; // Multiplication has higher precedence than addition
console.log(result); // Output: 14 (3 * 4 = 12, then 2 + 12 = 14)

result = (2 + 3) * 4; // Parentheses override precedence
console.log(result); // Output: 20 (2 + 3 = 5, then 5 * 4 = 20)

Chapter 3: Control Flow: Conditionals and Loops

3.1. Conditional Statements

Conditional statements allow you to execute different blocks of code depending on whether a certain condition is true or false.

  1. if, else if, else
  • The if statement executes a block of code if a condition is true.
  • The else if statement provides additional conditions to check if the initial if condition is false. You can have multiple else if statements.
  • The else statement executes a block of code if none of the preceding if or else if conditions are true. It is optional.
let age = 20;

if (age >= 18) {
console.log("You are an adult.");
} else if (age >= 13) {
console.log("You are a teenager.");
} else {
console.log("You are a child.");
}

Explanation:

  • The first if condition (age >= 18) is checked. Since age is 20, this condition is true.
  • The code block inside the first if statement (console.log(“You are an adult.”);) is executed.
  • The rest of the else if and else blocks are skipped because the if condition was true.

Example with boolean variables:

let isLoggedIn = true;
let isAdmin = false;

if (isLoggedIn && isAdmin) {
console.log("Welcome, administrator!");
} else if (isLoggedIn) {
console.log("Welcome, user!");
} else {
console.log("Please log in.");
}

2. Ternary Operator (Shorthand if…else):

For simple if…else statements, you can use the ternary operator:

let isMember = true;
let discount = isMember ? 0.1 : 0; // condition ? valueIfTrue : valueIfFalse
console.log("Discount:", discount); // Output: Discount: 0.1

let message = age >= 18 ? "You can vote" : "You cannot vote yet";
console.log(message); //Output: You can vote, assuming age is still 20

3. switch statement

The switch statement provides a more efficient way to handle multiple conditions based on the value of a single expression.

let day = "Monday";

switch (day) {
case "Monday":
console.log("Start of the week");
break;
case "Friday":
console.log("Almost weekend");
break;
default:
console.log("It's a regular day");
}

Explanation:

  1. The switch statement evaluates the expression day.
  2. The case labels are compared against the value of day.
  3. If a case label matches the value of day, the code block associated with that case is executed.
  4. The break statement is crucial. It prevents the code from “falling through” to the next case. If you omit the break statement, the code will continue executing into the next case even if it doesn’t match.
  5. The default case (optional) is executed if none of the case labels match the value of day.

Example without break (Fallthrough):

let number = 2;

switch (number) {
case 1:
console.log("One");
case 2:
console.log("Two");
case 3:
console.log("Three");
default:
console.log("Default");
}
// Output:
// Two
// Three
// Default

In this example, since there is no break statement after case 2, the code continues to execute the case 3 and default blocks. This fallthrough behavior can be useful in some cases, but it’s important to be aware of it to avoid unexpected results.

3.2. Loops

Loops allow you to repeat a block of code multiple times.

  1. for loop

The for loop is the most common type of loop. It has three parts:

  • Initialization: Executed once at the beginning of the loop (usually to declare a counter variable).
  • Condition: Checked before each iteration of the loop. The loop continues as long as the condition is true.
  • Increment/Decrement: Executed after each iteration of the loop (usually to update the counter variable).
for (let i = 0; i < 5; i++) {
console.log("Iteration:", i);
}
// Output:
// Iteration: 0
// Iteration: 1
// Iteration: 2
// Iteration: 3
// Iteration: 4

Explanation:

  • let i = 0;: The counter variable i is initialized to 0.
  • i < 5;: The condition i < 5 is checked. Since 0 is less than 5, the loop continues.
  • console.log(“Iteration:”, i);: The code inside the loop is executed.
  • i++;: The counter variable i is incremented by 1 (i becomes 1).
  • Steps 2–4 are repeated until i is no longer less than 5.

Example with an array:

const colors = ["red", "green", "blue"];

for (let i = 0; i < colors.length; i++) {
console.log("Color:", colors[i]);
}

2. while loop

The while loop executes a block of code as long as a condition is true. It’s simpler than the for loop, but you need to manually handle the initialization and increment/decrement of the counter variable.

let i = 0;

while (i < 5) {
console.log("Iteration:", i);
i++;
}
  • Important: Make sure your while loop condition eventually becomes false, or you’ll create an infinite loop that will crash your browser or Node.js process.

3. do…while loop

The do…while loop is similar to the while loop, but it always executes the code block at least once, even if the condition is initially false. The condition is checked after the code block is executed.

let i = 5;

do {
console.log("Iteration:", i);
i++;
} while (i < 5);

// Output: Iteration: 5 (the code block is executed once, even though i is not less than 5)

4. for…in loop (iterate over object properties)

The for…in loop iterates over the enumerable properties of an object. It assigns the property name (key) to the loop variable in each iteration.

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

for (let key in person) {
console.log("Key:", key, "Value:", person[key]);
}
// Output:
// Key: name Value: John
// Key: age Value: 30
// Key: city Value: New York

Important:

  • The order of iteration is not guaranteed to be the same as the order in which the properties were added to the object.
  • The for…in loop iterates over inherited properties as well (properties from the object’s prototype chain). To avoid iterating over inherited properties, you can use the hasOwnProperty() method:
for (let key in person) {
if (person.hasOwnProperty(key)) {
console.log("Key:", key, "Value:", person[key]);
}
}

5. for…of loop (iterate over iterable objects — arrays, strings, etc.)

The for…of loop iterates over the values of an iterable object (e.g., an array, a string, a Map, a Set, etc.). It’s simpler and more direct than using a for loop with an index when you only need the values.

const colors = ["red", "green", "blue"];

for (let color of colors) {
console.log("Color:", color);
}
// Output:
// Color: red
// Color: green
// Color: blue

const message = "Hello";

for (let char of message) {
console.log("Character:", char);
}
// Output:
// Character: H
// Character: e
// Character: l
// Character: l
// Character: o

Difference between for…in and for…of:

  • for…in: Iterates over the keys (property names) of an object.
  • for…of: Iterates over the values of an iterable object.

6. break and continue statements

  • break: Terminates the current loop or switch statement immediately.
for (let i = 0; i < 10; i++) {
if (i === 5) {
break; // Exit the loop when i is 5
}
console.log("Iteration:", i);
}
// Output:
// Iteration: 0
// Iteration: 1
// Iteration: 2
// Iteration: 3
// Iteration: 4
  • continue: Skips the rest of the current iteration of the loop and proceeds to the next iteration.
for (let i = 0; i < 10; i++) {
if (i % 2 === 0) {
continue; // Skip even numbers
}
console.log("Odd number:", i);
}
// Output:
// Odd number: 1
// Odd number: 3
// Odd number: 5
// Odd number: 7
// Odd number: 9

Chapter 4: Functions and Scope

In javascript, functions are first class citizen.

Functions are fundamental building blocks in JavaScript. They are reusable blocks of code that perform a specific task. Functions allow you to organize your code, make it more readable, and avoid repetition. Think of them as mini-programs within your larger program.

4.1 Function Declaration vs. Function Expression

There are two primary ways to define functions in JavaScript:

1. Function Declaration: This is the most common and straightforward way.

// Hoisting is applied
console.log(greet("Alice")); // Output: Hello, Alice!
function greet(name) { // Function declaration
return "Hello, " + name + "!";
}
  • Syntax: function functionName(parameters) { /* code to execute */ }
  • Hoisting: Function declarations are hoisted. This means you can call the function before it’s defined in your code. The JavaScript engine effectively moves the declaration to the top of the scope during compilation.

2. Function Expression: Here, a function is assigned to a variable.

// Hoisting is not applied,
console.log(greet("Bob")); // Throw error
const greet = function(name) { // Function expression
return "Hello, " + name + "!";
};
console.log(greet("Bob")); // Output: Hello, Alice!
  • Syntax: const functionName = function(parameters) { /* code to execute */ }; (You can also use let or var instead of const)
  • Hoisting: Function expressions are not hoisted. You must define the function expression before you call it. Trying to call sayHello before the line where it’s assigned will result in a ReferenceError.
  • Anonymous Functions: Notice that the function in a function expression often doesn’t have a name (it’s anonymous). You can give it a name, but it’s not generally necessary.

3. Key Difference (Hoisting):

// Function Declaration (works because of hoisting)
console.log(add(5, 3)); // Output: 8

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

// Function Expression (will cause an error)
// console.log(multiply(4, 2)); //Uncommenting this line will cause an error
const multiply = function(a, b) {
return a * b;
};
console.log(multiply(4,2)); //this works

When to use which:

  • Function declarations are often preferred for readability, especially for simple functions.
  • Function expressions are useful when you need to pass functions as arguments to other functions (callbacks), or when you want to create functions dynamically. They are essential for concepts like closures (covered later).

4.2. Function Parameters and Arguments

  • Parameters: These are the variables listed in the function definition’s parentheses. They act as placeholders for the values that will be passed into the function.
  • Arguments: These are the actual values you pass to the function when you call it.
function describePerson(name, age, occupation) { // name, age, occupation are parameters
return name + " is a " + age + "-year-old " + occupation + ".";
}
const description = describePerson("Charlie", 30, "Developer"); // "Charlie", 30, "Developer" are arguments
console.log(description); // Output: Charlie is a 30-year-old Developer.

Number of Arguments:

  • JavaScript doesn’t enforce the number of arguments you must pass.
  • If you pass fewer arguments than parameters, the unsupplied parameters will have the value undefined.
  • If you pass more arguments than parameters, the extra arguments are ignored (but accessible via the arguments object — more on that later).
function greet2(name, greeting) {
console.log("Name:", name);
console.log("Greeting:", greeting);
}
greet2("David"); // Output: Name: David, Greeting: undefined
greet2("Eve", "Good morning", "Extra!"); // Output: Name: Eve, Greeting: Good morning (the "Extra!" is ignored by parameter assignment)

4.3. Return Values

  • The return statement specifies the value that the function will send back to the caller.
  • A function can have multiple return statements, but only one will be executed. As soon as a return statement is encountered, the function stops executing.
  • If a function doesn’t have a return statement (or has a return statement without a value), it implicitly returns undefined.

function square(number) {

return number * number; // Returns the square of the number
}

const result = square(5);
console.log(result); // Output: 25

function noReturn() {
console.log("This function doesn't explicitly return anything.");
}
const voidResult = noReturn(); //calls the function which executes the code inside it
console.log(voidResult); // Output: undefined

4.4. Function Scope and Closure

  1. Scope: Scope determines the visibility and accessibility of variables within your code. There are two main types of scope in JavaScript:
  • Global Scope: Variables declared outside of any function have global scope. They can be accessed from anywhere in your code. Avoid using global variables excessively, as they can lead to naming conflicts and make your code harder to maintain.
  • Function Scope: Variables declared inside a function have function scope. They can only be accessed within that function.
  • Block Scope (introduced with let and const): Variables declared with let or const inside a block (e.g., inside an if statement, a for loop, or just a block delimited by {}) have block scope. They are only accessible within that block.
let globalVar = "I am global";
function myFunction() {
let functionVar = "I am function-scoped";
console.log(globalVar); // Accessible within the function
console.log(functionVar);
if (true) {
let blockVar = "I am block-scoped";
console.log(blockVar); // Accessible within the block
}
if(true) {
let secondVar = "I am block-scope 2";
console.log(secondVar);
}
// console.log(blockVar); // Error: blockVar is not defined outside the block
}
myFunction();
console.log(globalVar); // Accessible outside the function
// console.log(functionVar); // Error: functionVar is not defined outside the function

2. Closure: This is a more advanced concept, but the basic idea is that a function has access to variables in its lexical environment (the environment in which it was defined), even after the outer function has finished executing. This is a powerful mechanism for data encapsulation and creating private variables. We’ll cover this in more detail later.

function outerFunction() {
let outerVar = "Hello from outer!";
function innerFunction() {
console.log(outerVar); // innerFunction "remembers" outerVar
}
return innerFunction;
}
const myInnerFunction = outerFunction(); // outerFunction has finished executing
myInnerFunction(); // Output: Hello from outer! (Closure in action)

4.5. Arrow Functions (ES6)

Arrow functions provide a more concise syntax for writing functions.


function greet(name) { // Function declaration
return "Hello, " + name + "!";
}

const greet = function(name) { // Function as expression
return "Hello, " + name + "!";
};

const greet = name => { // Arrow function
return "Hello, " + name + "!";
};
console.log(greet("Bob")); // Output: Hello, Alice!
  • Syntax: const functionName = (parameters) => { /* code to execute */ }
  • Implicit Return: If the arrow function body is a single expression, the return keyword is implicit. In the example above, a + b is automatically returned. If you have a block of code within curly braces {}, you need to use the return keyword explicitly.
// Explicit return
const multiply = (a, b) => {
const result = a * b;
return result; // Explicit return required when using curly braces
};
console.log(multiply(4, 5)); // Output: 20
// Implicit return
const multiply = (a, b) => a*b;
console.log(multiply(4, 5)); // Output: 20
  • this Binding: Arrow functions do not have their own this binding. They inherit the this value from the surrounding context (lexical scope). This is often a desirable behavior and can avoid common this binding issues.
  • No arguments Object: Arrow functions do not have the arguments object (see below).

4.6. Immediately Invoked Function Expressions (IIFE)

An IIFE is a function that is defined and executed immediately. They are often used to create a private scope to avoid polluting the global namespace.

This is also called self executable function.

(function() {
let privateVar = "This is private";
console.log("IIFE executed");
// ... other code that uses privateVar ...
})(); // The parentheses at the end invoke the function

// console.log(privateVar); // Error: privateVar is not defined (it's only accessible within the IIFE)
  • Syntax: (function() { /* code */ })(); or !function() { /* code */ }(); (There are other variations, but these are common.)
  • The parentheses around the function keyword are crucial. They tell the JavaScript parser to treat the function as an expression rather than a declaration.
  • The parentheses at the end () invoke the function.

4.7. Default Parameters (ES6)

Default parameters allow you to specify default values for function parameters if no argument is provided when the function is called.

function greet3(name = "Guest") { // Default parameter
return "Hello, " + name + "!";
}
console.log(greet3("Frank")); // Output: Hello, Frank!
console.log(greet3()); // Output: Hello, Guest!
  • Default parameters make your functions more flexible and prevent errors if a required argument is missing.
  • You can use expressions as default values:
function calculateTax(price, taxRate = 0.07 * price) {
return price + taxRate;
}
console.log(calculateTax(100)); //Uses the default taxrate expression

4.8. Rest Parameters (ES6)

Rest parameters allow you to represent an indefinite number of arguments as an array.

function sum(...numbers) { // Rest parameter
let total = 0;
for (let number of numbers) {
total += number;
}
return total;
}

console.log(sum(1, 2, 3, 4, 5)); // Output: 15
console.log(sum(10, 20)); // Output: 30
  • Syntax: …parameterName
  • The rest parameter must be the last parameter in the function definition.
  • The numbers variable in the example above will be an array containing all the arguments passed to the function after the explicitly defined parameters (if any).

4.9. The arguments Object (Older Style)

Before rest parameters were introduced, the arguments object was used to access all the arguments passed to a function. It’s an array-like object (but not a true array) that contains all the arguments. While still available for compatibility reasons, rest parameters are generally preferred because they are more readable and offer better type safety.

function logArguments() {
console.log(arguments);
for (let i = 0; i < arguments.length; i++) {
console.log("Argument " + i + ": " + arguments[i]);
}
}

logArguments("Apple", "Banana", "Cherry");
// Output:
// Arguments { 0: 'Apple', 1: 'Banana', 2: 'Cherry' }
// Argument 0: Apple
// Argument 1: Banana
// Argument 2: Cherry

Important Notes about arguments:

  • It’s an array-like object, not a true array. You can access elements by index (e.g., arguments[0]), but you can’t directly use array methods like push() or pop() on it. You can convert it to a true array using Array.from(arguments) or the spread syntax […arguments].
  • Arrow functions do not have the arguments object.

Example Combining Concepts:

function createGreeting(greeting = "Hello", ...names) {
if (names.length === 0) {
return greeting + ", world!";
}
return greeting + ", " + names.join(" and ") + "!";
}
console.log(createGreeting()); // Hello, world!
console.log(createGreeting("Hi", "John")); // Hi, John!
console.log(createGreeting("Greetings", "Alice", "Bob", "Charlie")); // Greetings, Alice and Bob and Charlie!

This example uses a default parameter (greeting), a rest parameter (names), and string concatenation to create a flexible greeting function.

Chapter 5: Objects data structure

5.1. Creating Objects

JavaScript objects are collections of key-value pairs, where the keys are strings (or Symbols) and the values can be any JavaScript data type (numbers, strings, booleans, arrays, other objects, functions, etc.).

  • Object Literal (Most Common): This is the simplest and most common way to create objects.
const person = {
firstName: "John",
lastName: "Doe",
age: 30,
occupation: "Engineer"
};

console.log(person); // Output: { firstName: 'John', lastName: 'Doe', age: 30, occupation: 'Engineer' }
  • new Object() Constructor: Less common, but functionally equivalent to the object literal.
const person = new Object();
person.firstName = "John";
person.lastName = "Doe";
person.age = 30;
person.occupation = "Engineer"

console.log(person); // Output: { firstName: 'John', lastName: 'Doe', age: 30, occupation: 'Engineer' }

5.2. Accessing Object Properties

There are two main ways to access the properties of an object:

  • Dot Notation: Use the dot (.) operator followed by the property name. This is the preferred method when the property name is known and is a valid JavaScript identifier (starts with a letter, underscore, or dollar sign; and contains only letters, numbers, underscores, or dollar signs).
console.log(person.firstName); // Output: "John"
console.log(person.age); // Output: 30
  • Bracket Notation: Use square brackets ([]) with the property name enclosed in quotes. This is necessary when the property name is not a valid JavaScript identifier (e.g., contains spaces, starts with a number, or is a variable).
const myObj = {
"first name": "Alice", // Property name with a space
123: "A number as a key", // Numeric property name
address: {
street: "123 Main St"
}
};

console.log(myObj["first name"]); // Output: "Alice"
console.log(myObj[123]); // Output: "A number as a key"

const propertyName = "address";
console.log(myObj[propertyName].street); // Output: "123 Main St" (using a variable)

5.3. Adding, Modifying, and Deleting Properties

  • Adding Properties: You can add properties to an existing object using dot notation or bracket notation.
person.email = "john.doe@example.com"; // Adding a property with dot notation

console.log(person); // Output includes email
  • Modifying Properties: You can modify the value of an existing property using dot notation or bracket notation.
person.age = 31;      // Modifying with dot notation

console.log(person.age); // Output: 31
  • Deleting Properties: Use the delete operator to remove a property from an object.
delete person.occupation; // Deleting with dot notation

console.log(person); // Output no longer includes occupation

5.4. Object Methods

Object methods are functions that are associated with an object. They are defined as properties of the object, with the value being a function.

const student = {
firstName: "Bob",
lastName: "Smith",
grades: [80, 90, 75],
getFullName: function() { // Method defined as a function expression
return this.firstName + " " + this.lastName;
},
getAverageGrade: function() {
let sum = 0;
for (let i = 0; i < this.grades.length; i++) {
sum += this.grades[i];
}
return sum / this.grades.length;
}
};

console.log(student.getFullName()); // Output: "Bob Smith"
console.log(student.getAverageGrade()); // Output: 81.66666666666667

5.5. The this Keyword (Basic Understanding)

Inside an object method, the this keyword refers to the object itself. This allows you to access and manipulate the object’s properties from within the method. The value of this can be tricky in JavaScript and will be covered in more detail in later chapters, especially regarding arrow functions and event handlers.

In the student example above, this.firstName refers to the firstName property of the student object.

5.6. Object Destructuring (ES6)

Object destructuring is a convenient way to extract values from an object and assign them to variables.

const product = {
id: 123,
name: "Laptop",
price: 1200,
category: "Electronics"
};

// Destructuring assignment
/**
const id = product.id
const name = product.name;
const price = product.price;
**/
const { id, name, price } = product;

console.log(id); // Output: 123
console.log(name); // Output: "Laptop"
console.log(price); // Output: 1200

//Destructuring with renaming
const {name: productName, price: productPrice} = product;
console.log(productName); //Laptop
console.log(productPrice); //1200

//Destructuring with default values
const {discount = 0.1} = product;
console.log(discount) // 0.1

5.7. Object Spread Syntax (ES6)

The spread syntax (…) allows you to create a new object by copying properties from an existing object. It’s useful for creating shallow copies of objects or for merging objects.

const originalBook = {
title: "The Hobbit",
author: "J.R.R. Tolkien",
year: 1937
};

// Creating a shallow copy using spread syntax
const copiedBook = { ...originalBook };
copiedBook.year = 1954; // Modify the copied object

console.log(originalBook); // Output: { title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1937 }
console.log(copiedBook); // Output: { title: 'The Hobbit', author: 'J.R.R. Tolkien', year: 1954 }

// Merging objects
const additionalInfo = {
publisher: "Allen & Unwin",
pages: 310
};

const completeBook = { ...originalBook, ...additionalInfo };

console.log(completeBook); // Output includes all properties from originalBook and additionalInfo
// {
// title: 'The Hobbit',
// author: 'J.R.R. Tolkien',
// year: 1937,
// publisher: 'Allen & Unwin',
// pages: 310
// }

5.8. Object.keys(), Object.values(), Object.entries()

These methods provide ways to iterate over the properties of an object.

  • Object.keys(obj): Returns an array of the object’s keys (property names).
const keys = Object.keys(person);
console.log(keys); // Output: ["firstName", "lastName", "age", "email"]
  • Object.values(obj): Returns an array of the object’s values.
const values = Object.values(person);
console.log(values); // Output: ["John", "Doe", 31, "john.doe@example.com"]
  • Object.entries(obj): Returns an array of key-value pairs, where each pair is an array [key, value].
const entries = Object.entries(person);
console.log(entries);
// Output:
// [
// ["firstName", "John"],
// ["lastName", "Doe"],
// ["age", 31],
// ["email", "john.doe@example.com"]
// ]

Using these methods to iterate over an object:

const myObject = { a: 1, b: 2, c: 3 };

// Using Object.keys()
Object.keys(myObject).forEach(key => {
console.log(`Key: ${key}, Value: ${myObject[key]}`);
});

// Using Object.entries()
Object.entries(myObject).forEach(([key, value]) => {
console.log(`Key: ${key}, Value: ${value}`);
});

Chapter 6: Arrays

6.1. Creating Arrays

Arrays are ordered collections of values. These values can be of any data type, including numbers, strings, booleans, objects, and even other arrays.

  • Array Literal (Most Common): The preferred and most concise way to create arrays.
const numbers = [1, 2, 3, 4, 5];
const fruits = ["apple", "banana", "orange"];
const mixed = [1, "hello", true, { name: "John" }];

console.log(numbers); // Output: [1, 2, 3, 4, 5]
console.log(fruits); // Output: ["apple", "banana", "orange"]
console.log(mixed); // Output: [1, "hello", true, { name: "John" }]
  • new Array() Constructor: Less common and generally not recommended, as it can be ambiguous.
const colors = new Array("red", "green", "blue");  // Initializing with values
const emptyArray = new Array(5); // Creates an array of length 5 with undefined values

console.log(colors); // Output: ["red", "green", "blue"]
console.log(emptyArray); // Output: [undefined, undefined, undefined, undefined, undefined]

6.2. Accessing Array Elements (Index)

Array elements are accessed using their index, which is a zero-based integer.

const animals = ["dog", "cat", "bird"];

console.log(animals[0]); // Output: "dog"
console.log(animals[1]); // Output: "cat"
console.log(animals[2]); // Output: "bird"

//Access array from variable
const index = 1;
console.log(animals[index]); //Output cat

//Trying to access an index that doesn't exist will return undefined.
console.log(animals[3]); //Output undefined

6.3. Array Properties (e.g., length)

The most common array property is length, which returns the number of elements in the array.

const names = ["Alice", "Bob", "Charlie"];
console.log(names.length); // Output: 3

// You can also use the length property to add elements to the end of the array.
names[names.length] = "David";
console.log(names); // Output: ["Alice", "Bob", "Charlie", "David"]

6.4. Array Methods

These methods allow you to modify and manipulate arrays.

6.4.1 Adding and Removing Elements:

  • push(element1, …, elementN): Adds one or more elements to the end of the array and returns the new length of the array.
const fruits = ["apple", "banana"];
fruits.push("orange", "grape");
console.log(fruits); // Output: ["apple", "banana", "orange", "grape"]
  • pop(): Removes the last element from the array and returns that element. If the array is empty, it returns undefined.
const fruits = ["apple", "banana", "orange"];
const lastFruit = fruits.pop();
console.log(fruits); // Output: ["apple", "banana"]
console.log(lastFruit); // Output: "orange"
  • shift(): Removes the first element from the array and returns that element. If the array is empty, it returns undefined.
const fruits = ["apple", "banana", "orange"];
const firstFruit = fruits.shift();
console.log(fruits); // Output: ["banana", "orange"]
console.log(firstFruit); // Output: "apple"
  • unshift(element1, …, elementN): Adds one or more elements to the beginning of the array and returns the new length of the array.
const fruits = ["banana", "orange"];
fruits.unshift("apple", "kiwi");
console.log(fruits); // Output: ["apple", "kiwi", "banana", "orange"]

6.4.2 splice() and slice()

  1. splice(start, deleteCount, item1, …, itemN): Modifies the array by removing or replacing existing elements and/or adding new elements. It returns an array containing the deleted elements.
  • start: The index at which to start modifying the array.
  • deleteCount: The number of elements to remove from the start index. If deleteCount is 0, no elements are removed.
  • item1, …, itemN: Optional. The elements to add to the array, beginning at the start index.
const numbers = [1, 2, 3, 4, 5];
const removed = numbers.splice(2, 2, 6, 7); // Remove 2 elements starting at index 2, add 6 and 7
console.log(numbers); // Output: [1, 2, 6, 7, 5]
console.log(removed); // Output: [3, 4]

const letters = ['a','b','c','d','e'];
letters.splice(2,0,'f','g');
console.log(letters); //Output ['a', 'b', 'f', 'g', 'c', 'd', 'e']

2. slice(start, end): Returns a new array containing a portion of the original array. It does not modify the original array.

  • start: The index at which to start the slice (inclusive).
  • end: The index at which to end the slice (exclusive). If omitted, slice extracts to the end of the array.
const letters = ["a", "b", "c", "d", "e"];
const sliced = letters.slice(1, 4); // Elements from index 1 up to (but not including) index 4
console.log(sliced); // Output: ["b", "c", "d"]
console.log(letters); // Output: ["a", "b", "c", "d", "e"] (original array is unchanged)

6.4.3 concat() and join()

  • concat(value1, value2, …, valueN): Returns a new array that is the result of joining the original array with other array(s) and/or value(s). It does not modify the original array.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = arr1.concat(arr2, 7, 8);
console.log(arr3); // Output: [1, 2, 3, 4, 5, 6, 7, 8]
console.log(arr1); //Output [1, 2, 3]

const arr4 = [9,10];
const arr5 = arr3.concat(arr4);
console.log(arr5); //Output [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  • join(separator): Returns a string by concatenating all of the elements in an array, separated by the specified separator. If no separator is provided, a comma (,) is used.
const words = ["Hello", "world", "!"];
const sentence = words.join(" "); // Join with a space
console.log(sentence); // Output: "Hello world !"

const numbers = [1,2,3,4];
const commaNumber = numbers.join(",");
console.log(commaNumber); //Output 1,2,3,4

const plusNumber = numbers.join("+");
console.log(plusNumber); //Output 1+2+3+4

6.4.5 indexOf(), lastIndexOf(), and includes()

  1. indexOf(searchElement, fromIndex): Returns the first index at which a given searchElement can be found in the array, or -1 if it is not present.
  • searchElement: The element to search for.
  • fromIndex: Optional. The index to start the search from.
const numbers = [10, 20, 30, 20, 40];
console.log(numbers.indexOf(20)); // Output: 1
console.log(numbers.indexOf(20, 2)); // Output: 3 (search starting from index 2)
console.log(numbers.indexOf(50)); // Output: -1 (not found)

2. lastIndexOf(searchElement, fromIndex): Returns the last index at which a given searchElement can be found in the array, or -1 if it is not present. The search is performed backwards from fromIndex.

const numbers = [10, 20, 30, 20, 40];
console.log(numbers.lastIndexOf(20)); // Output: 3
console.log(numbers.lastIndexOf(20, 2)); // Output: 1 (search backwards from index 2)
console.log(numbers.lastIndexOf(50)); // Output: -1 (not found)

3. includes(searchElement, fromIndex) (ES7): Returns true if the array contains the searchElement, false otherwise.

const fruits = ["apple", "banana", "orange"];
console.log(fruits.includes("banana")); // Output: true
console.log(fruits.includes("grape")); // Output: false
console.log(fruits.includes("apple", 1)); // Output: false (search starting from index 1)

6.5. Iterating over Arrays

There are several ways to iterate over the elements of an array.

  1. for Loop: The traditional approach.
const numbers = [1, 2, 3, 4, 5];
for (let i = 0; i < numbers.length; i++) {
console.log(`Index: ${i}, Value: ${numbers[i]}`);
}

2. for…of Loop (ES6): A more concise way to iterate over the values of an array.

const colors = ["red", "green", "blue"];
for (const color of colors) {
console.log(color);
}

3. forEach(callback): Executes a provided function once for each array element.

const names = ["Alice", "Bob", "Charlie"];
names.forEach((name, index) => { //callback function with parameters name and index
console.log(`Index: ${index}, Name: ${name}`);
});

//Example without index parameters
names.forEach(name => { //callback function with parameters name and index
console.log(`Name: ${name}`);
});

4. map(callback): Creates a new array with the results of calling a provided function on every element in the original array.

const numbers = [1, 2, 3, 4, 5];
const squared = numbers.map(number => number * 2);
console.log(squared); // Output: [2, 4, 6, 8, 10]
console.log(numbers); // Output: [1, 2, 3, 4, 5] (original array unchanged)

5. filter(callback): Creates a new array with all elements that pass the test implemented by the provided function.

const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter(number => number % 2 === 0);
console.log(evenNumbers); // Output: [2, 4, 6]

6. reduce(callback, initialValue): Executes a reducer function (provided as a callback) on each element of the array, resulting in a single output value.

callback(accumulator, currentValue, currentIndex, array): The reducer function.

  • accumulator: The accumulated value from the previous call to the callback. It’s the initialValue on the first call.
  • currentValue: The current element being processed.
  • currentIndex: The index of the current element.
  • array: The array being traversed.

initialValue: Optional. A value to use as the first argument to the first call of the callback. If no initialValue is supplied, the first element in the array will be used as the initial accumulator value and skipped as currentValue. Calling reduce() on an empty array without an initial value will throw a TypeError.

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0); // 0 is the initialValue
console.log(sum); // Output: 15

//Without initialValue.
const multiplyNumbers = [1,2,3,4,5];
const totalMultiply = multiplyNumbers.reduce((accumulator, currentValue) => accumulator * currentValue)
console.log(totalMultiply) // Output: 120

7. every(callback): Tests whether all elements in the array pass the test implemented by the provided function. Returns true if all elements pass the test, false otherwise.

const numbers = [2, 4, 6, 8, 10];
const allEven = numbers.every(number => number % 2 === 0);
console.log(allEven); // Output: true

const mixedNumbers = [2,4,6,7,8]
const mixedEven = mixedNumbers.every(number => number % 2 === 0);
console.log(mixedEven); //Output: false

8. some(callback): Tests whether at least one element in the array passes the test implemented by the provided function. Returns true if at least one element passes the test, false otherwise.

const numbers = [1, 3, 5, 8, 9];
const hasEven = numbers.some(number => number % 2 === 0);
console.log(hasEven); // Output: true

const oddNumbers = [1,3,5,7,9];
const oddEven = oddNumbers.some(number => number % 2 === 0);
console.log(oddEven); // Output: false

6.6 Array Destructuring

Array destructuring is a concise way to extract values from an array and assign them to variables.

const coordinates = [10, 20, 30];
const [x, y, z] = coordinates;

console.log(x); // Output: 10
console.log(y); // Output: 20
console.log(z); // Output: 30

//Skipping array Item
const [first,,third] = coordinates;
console.log(first); //Output 10
console.log(third); //Output 30

// Using the Rest Operator to collect remaining elements:
const [firstCoordinate, secondCoordinate, ...restCoordinates] = coordinates;
console.log(firstCoordinate); //Output 10
console.log(secondCoordinate); //Output 20
console.log(restCoordinates); //Output [30]

6.7 Multi-Dimensional Arrays

A multi-dimensional array is simply an array where each element is itself another array. This allows you to represent data in a grid-like structure (rows and columns) or even higher dimensions.

  1. Creating Multi-Dimensional Arrays
// 2D Array (Matrix)
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];

console.log(matrix);
/* Output:
[
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
*/

// 3D Array (Cube) - Less Common but Possible
const cube = [
[
[1, 2],
[3, 4]
],
[
[5, 6],
[7, 8]
]
];

console.log(cube);

/* Output:
[
[ [ 1, 2 ], [ 3, 4 ] ],
[ [ 5, 6 ], [ 7, 8 ] ]
]
*/

2. Accessing Elements in Multi-Dimensional Arrays

You use multiple indexes to access elements in a multi-dimensional array. For a 2D array, you use one index for the row and another for the column.

console.log(matrix[0][0]); // Output: 1 (Row 0, Column 0)
console.log(matrix[1][2]); // Output: 6 (Row 1, Column 2)
console.log(matrix[2][1]); // Output: 8 (Row 2, Column 1)

//Access the cube array
console.log(cube[0][1][1]); //Output 4
console.log(cube[1][0][0]); //Output 5

3. Iterating Over Multi-Dimensional Arrays

You typically need nested loops to iterate over all elements in a multi-dimensional array.

  • Using for Loops:
// 2D Array Iteration
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
console.log(`Element at [${i}][${j}]: ${matrix[i][j]}`);
}
}
  • Using forEach():
matrix.forEach((row, rowIndex) => {
row.forEach((element, columnIndex) => {
console.log(`Element at [${rowIndex}][${columnIndex}]: ${element}`);
});
});

Note: We can apply looping similar to 1d array to 2d.

Chapter 7: Map and Set data structure

7.1. Map Data Structure

A Map is an object that holds key-value pairs, similar to a regular JavaScript object. However, unlike objects, Map keys can be any data type (including objects, functions, and primitive values), and Map preserves the order of key insertion. This makes Map more versatile and predictable than objects when you need to store and retrieve data based on diverse keys or maintain insertion order.

7.1.1 Key Concepts:

  • Keys can be any data type: Objects, primitives, even other Maps.
  • Ordered: Iteration through a Map occurs in the order of key insertion.
  • Size: You can easily determine the number of key-value pairs using the size property.
  • Iteration: Maps are easily iterable using for…of loops.

7.1.2 Popular Map APIs:

  • new Map(): Creates a new Map object.
// Creating a Map
const myMap = new Map();
  • set(key, value): Adds a new key-value pair to the Map or updates the value for an existing key. Returns the Map object itself, allowing for chaining.
// Creating a Map
const myMap = new Map();

// Setting key-value pairs
myMap.set("name", "Alice");
myMap.set(1, "Number One");
myMap.set({}, "Empty Object");
myMap.set(function() {}, "A Function");
  • get(key): Returns the value associated with the specified key. Returns undefined if the key is not found.
// Creating a Map
const myMap = new Map();

// Setting key-value pairs
myMap.set("name", "Alice");
myMap.set(1, "Number One");
myMap.set({}, "Empty Object");
myMap.set(function() {}, "A Function");

console.log(myMap); // Output: Map(4) { 'name' => 'Alice', 1 => 'Number One', {} => 'Empty Object', [Function (anonymous)] => 'A Function' }

// Getting values
console.log(myMap.get("name")); // Output: Alice
console.log(myMap.get(1)); // Output: Number One
console.log(myMap.get({})); // Output: undefined (because the key is a different object, even though it looks the same)
  • has(key): Returns a boolean indicating whether a key exists in the Map.
// Creating a Map
const myMap = new Map();

// Setting key-value pairs
myMap.set("name", "Alice");
myMap.set(1, "Number One");
myMap.set({}, "Empty Object");
myMap.set(function() {}, "A Function");

// Checking if a key exists
console.log(myMap.has("name")); // Output: true
console.log(myMap.has("age")); // Output: false
  • size: A property that returns the number of key-value pairs in the Map.
// Creating a Map
const myMap = new Map();

// Setting key-value pairs
myMap.set("name", "Alice");
myMap.set(1, "Number One");
myMap.set({}, "Empty Object");
myMap.set(function() {}, "A Function");

console.log(myMap); // Output: Map(4) { 'name' => 'Alice', 1 => 'Number One', {} => 'Empty Object', [Function (anonymous)] => 'A Function' }

// Getting the size
console.log(myMap.size); // Output: 4
  • delete(key): Removes the key-value pair associated with the specified key. Returns true if the key existed and was removed, false otherwise.
// Creating a Map
const myMap = new Map();

// Setting key-value pairs
myMap.set("name", "Alice");
myMap.set(1, "Number One");
myMap.set({}, "Empty Object");
myMap.set(function() {}, "A Function");

// Getting the size
console.log(myMap.size); // Output: 4

// Deleting a key-value pair
myMap.delete(1);
console.log(myMap.size); // Output: 3
console.log(myMap.has(1)); // Output: false
  • clear(): Removes all key-value pairs from the Map.
// Creating a Map
const myMap = new Map();

// Setting key-value pairs
myMap.set("name", "Alice");
myMap.set(1, "Number One");
myMap.set({}, "Empty Object");
myMap.set(function() {}, "A Function");

// Getting the size
console.log(myMap.size); // Output: 4

// Clearing the Map
myMap.clear();
console.log(myMap.size); // Output: 0
  • Iterate using for-of
// Iterating through a Map using for...of
for (const [key, value] of myMap) {
console.log(`Key: ${key}, Value: ${value}`);
}
// Output:
// Key: name, Value: Alice
// Key: [object Object], Value: Empty Object
// Key: function() {}, Value: A Function
  • keys(): Returns an iterator object that yields the keys in the Map in the order of insertion.
// Iterating through keys
for (const key of myMap.keys()) {
console.log("Key:", key);
}
  • values(): Returns an iterator object that yields the values in the Map in the order of insertion.
// Iterating through values
for (const value of myMap.values()) {
console.log("Value:", value);
}
  • entries(): Returns an iterator object that yields [key, value] pairs in the Map in the order of insertion. This is the default iterator for Map.
// Iterating through a Map using for...of
for (const [key, value] of myMap.entries()) {
console.log(`Key: ${key}, Value: ${value}`);
}
// Output:
// Key: name, Value: Alice
// Key: [object Object], Value: Empty Object
// Key: function() {}, Value: A Function
  • forEach(callbackFn, thisArg): Executes a provided function once for each key-value pair in the Map, in insertion order.
// Using forEach
myMap.forEach((value, key) => {
console.log(`Key: ${key}, Value: ${value}`);
});

7.1.3 Use Cases for Maps:

  • Storing metadata associated with DOM elements: You can use DOM elements as keys and store associated data as values.
  • Implementing caches: Maps are well-suited for caching data based on complex keys.
  • Counting occurrences of items: You can use a Map to count how many times each item appears in a dataset.
  • Lookup tables: Maps provide efficient key-based lookups.

7.2. Set Data Structure

A Set is an object that lets you store unique values of any data type, whether primitive values or object references. It’s similar to an array, but it automatically eliminates duplicate values. Set also preserves insertion order.

7.2.1 Key Concepts:

  • Unique Values: A Set only stores unique values; duplicates are automatically ignored.
  • Any Data Type: Sets can store any data type, just like Maps.
  • Ordered: Iteration happens in the order of value insertion.
  • Size: You can easily get the number of elements using the size property.
  • Iteration: Sets are easily iterable using for…of loops.

7.2.2 Popular Set APIs:

  • new Set(): Creates a new Set object.
// Creating a Set
const mySet = new Set();
  • add(value): Adds a new value to the Set. If the value already exists, it’s not added again. Returns the Set object itself, allowing chaining.
// Creating a Set
const mySet = new Set();

// Adding values
mySet.add(1);
mySet.add(2);
mySet.add(3);
mySet.add(2); // Duplicate - will be ignored
mySet.add("hello");
mySet.add({}); // Each {} is a new object even if the content is same
mySet.add({});

console.log(mySet); // Output: Set(6) { 1, 2, 3, 'hello', {}, {} }
  • delete(value): Removes a value from the Set. Returns true if the value existed and was removed, false otherwise.
// Deleting a value
mySet.delete(2);
  • has(value): Returns a boolean indicating whether a value exists in the Set.
// Checking if a value exists
console.log(mySet.has(2)); // Output: true
console.log(mySet.has(4)); // Output: false
  • Iterate using for-of
// Iterating through a Set using for...of
for (const value of mySet) {
console.log("Value:", value);
}
  • clear(): Removes all values from the Set.
// Clearing the Set
mySet.clear();
  • size: A property that returns the number of values in the Set.
// Creating a Set
const mySet = new Set();

// Adding values
mySet.add(1);
mySet.add(2);
mySet.add(3);
mySet.add(2); // Duplicate - will be ignored
mySet.add("hello");
mySet.add({}); // Each {} is a new object even if the content is same
mySet.add({});

// Getting the size
console.log(mySet.size); // Output: 6
  • values(): Returns an iterator object that yields the values in the Set in the order of insertion. This is the default iterator for Set.
// Iterating through a Set using for...of
for (const value of mySet.values()) {
console.log("Value:", value);
}
  • keys(): Returns an iterator object that yields the values in the Set in the order of insertion. Note: For Sets, keys() and values() return the same iterator (for compatibility with Maps).
// Iterating through a Set using for...of
for (const value of mySet.keys()) {
console.log("Value:", value);
}
  • entries(): Returns an iterator object that yields [value, value] pairs in the Set in the order of insertion. This is also for compatibility with Maps.
// Iterating through a Set using for...of
for (const value of mySet.entries()) {
console.log("Value:", value);
}
  • forEach(callbackFn, thisArg): Executes a provided function once for each value in the Set, in insertion order.
// Using forEach
mySet.forEach(value => {
console.log("Value:", value);
});

7.2.3 Use Cases for Sets:

Removing duplicates from an array: You can easily create a Set from an array to eliminate duplicates.

const numbersWithDuplicates = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = [...new Set(numbersWithDuplicates)]; // Using spread syntax to convert back to an array
console.log(uniqueNumbers); // Output: [1, 2, 3, 4, 5]
  • Checking for membership: Sets provide efficient membership checks (has() method).
  • Performing set operations: While JavaScript’s Set doesn’t have built-in methods for set operations like union, intersection, and difference, you can implement these operations using the Set API.

Example: Set Operations

// Union of two sets
function setUnion(setA, setB) {
const unionSet = new Set(setA);
for (const elem of setB) {
unionSet.add(elem);
}
return unionSet;
}

// Intersection of two sets
function setIntersection(setA, setB) {
const intersectionSet = new Set();
for (const elem of setB) {
if (setA.has(elem)) {
intersectionSet.add(elem);
}
}
return intersectionSet;
}

// Difference of two sets (A - B)
function setDifference(setA, setB) {
const differenceSet = new Set(setA);
for (const elem of setB) {
differenceSet.delete(elem);
}
return differenceSet;
}

const setA = new Set([1, 2, 3, 4, 5]);
const setB = new Set([3, 4, 5, 6, 7]);

console.log("Union:", setUnion(setA, setB)); // Output: Set(7) { 1, 2, 3, 4, 5, 6, 7 }
console.log("Intersection:", setIntersection(setA, setB)); // Output: Set(3) { 3, 4, 5 }
console.log("Difference (A-B):", setDifference(setA, setB)); // Output: Set(2) { 1, 2 }

7.3 Key Differences between Map and Set

Chapter 8: Error Handling and Debugging

8.1 try…catch…finally

The try…catch…finally statement is the foundation of error handling in JavaScript.

try {
undefinedFunction(); // This will throw a ReferenceError
console.log("This line will not be executed if undefinedFunction() throws an error.");
} catch (error) {
// Handle the error
console.error("An error occurred:", error.message); // Access the error message
console.error("Error name:", error.name); // Access the error name
console.error("Stack trace:", error.stack); // Access the stack trace (where the error happened)
} finally {
// Code that always executes
console.log("Finally block executed.");
}
console.log("Program continues after the try...catch...finally block.");
  • try block: This block contains the code that you suspect might throw an error.
  • catch block: This block is executed if an error occurs within the try block. The catch block receives an error object as an argument, allowing you to examine and handle the error.
  • finally block: This block is always executed, regardless of whether an error occurred in the try block or not. It’s typically used for cleanup operations (e.g., closing files, releasing resources).

Note: Divide by zero does not throw exception in javascript

try {
// Code that might throw an error
let result = 10 / 0; // This will result in Infinity, but doesn't throw an error!
console.log("Result:", result);
} catch (error) {
// Handle the error
console.error("An error occurred:", error.message); // Access the error message
} finally {
// Code that always executes
console.log("Finally block executed.");
}
console.log("Program continues after the try...catch...finally block.");
  • Program Flow: If an error occurs in the try block and is caught by the catch block, the program continues executing after the finally block. If an error is not caught, the program will usually terminate (or be handled by the browser/Node.js runtime).

9.2. throw Statement

The throw statement allows you to explicitly throw an error. You can throw any JavaScript value as an error (number, string, object), but it’s best practice to throw an Error object or a subclass of Error.

function checkAge(age) {
if (typeof age !== 'number') {
throw new TypeError("Age must be a number."); // Throw a TypeError
}
if (age < 0) {
throw new RangeError("Age cannot be negative."); // Throw a RangeError
}
if (age < 18) {
throw new Error("You must be at least 18 years old."); // Throw a generic Error
}
return "Access granted.";
}
try {
let result = checkAge(-5); // Will throw a RangeError
console.log(result); // This line won't execute if an error is thrown
} catch (error) {
console.error("Age check error:", error.message);
}

Explanation:

  • The checkAge function uses throw to explicitly raise errors when the input age is invalid.
  • The try…catch block handles the potential errors that checkAge might throw.

9.3. Error Objects (e.g., Error, TypeError, ReferenceError)

JavaScript has several built-in error types, all inheriting from the base Error object. Using the appropriate error type can provide more specific information about the nature of the error.

  • Error: A generic error. Use this when a more specific error type doesn’t apply.
  • TypeError: Indicates that an operand or argument is not of the expected type.
  • ReferenceError: Indicates that you’re trying to use a variable that hasn’t been declared.
  • SyntaxError: Indicates a syntax error in your code (usually caught during parsing).
  • RangeError: Indicates that a value is outside an allowed range.
  • URIError: Indicates that there’s a problem with the encodeURI() or decodeURI() functions.
  • EvalError: (Deprecated) Indicates an error in the use of the eval() function.
try {
//Example of each error
//let result = 10.toFixed(-1); // RangeError: toFixed() digits argument must be between 0 and 100
//console.log(nonExistentVariable); // ReferenceError: nonExistentVariable is not defined
//[].push.call(); //TypeError: Function.prototype.call was called on null which is a primitive and not a function
//eval('foo bar'); //SyntaxError: Unexpected identifier 'bar'
} catch (error) {
console.error("An error occurred:", error.message); // Access the error message
console.error("Error name:", error.name); // Access the error name
console.error("Stack trace:", error.stack); // Access the stack trace (where the error happened)
}

9.4. Custom Error Classes

You can create your own error classes by extending the Error object. This is useful for creating errors that are specific to your application or library.

class ValidationError extends Error {
constructor(message, field) {
super(message); // Call the Error constructor
this.name = "ValidationError"; // Set the error name
this.field = field; // Add a custom property (the field that caused the error)
}
}
function validateEmail(email) {
if (!email.includes("@")) {
throw new ValidationError("Invalid email format", "email");
}
// More validation logic could go here
}
try {
validateEmail("test");
} catch (error) {
if (error instanceof ValidationError) {
console.error("Validation error:", error.message, "Field:", error.field);
} else {
console.error("An unexpected error occurred:", error.message);
}
}

Explanation:

  • We create a ValidationError class that extends Error.
  • The constructor takes a message and a field argument. We call super(message) to initialize the Error object with the message.
  • We set the name property to “ValidationError” (this is good practice).
  • We add a custom field property to the error object, indicating which field caused the validation error.
  • In the catch block, we use instanceof to check if the error is a ValidationError. This allows us to handle ValidationError errors differently from other types of errors.

Chapter 9: String

Strings are sequences of characters used to represent text in JavaScript. They are immutable, meaning that string methods don’t modify the original string but return a new string with the changes.

9.1. String Literals

String literals are ways to define strings in JavaScript:

  • Single Quotes (‘…’):
const singleQuotedString = 'This is a string using single quotes.';
console.log(singleQuotedString);
  • Double Quotes (“…”):
const doubleQuotedString = "This is a string using double quotes.";
console.log(doubleQuotedString);
  • Template Literals (…) (ES6): Template literals offer powerful features like string interpolation and multiline strings.
const templateLiteralString = `This is a string using template literals.`;
console.log(templateLiteralString);

const multiline = `Hello Students,
Don't afraid to try new approach.
Until you don't try, you will never become comfirtable.
`;
console.log(templateLiteralString);

When to use which:

  • Single and double quotes are generally interchangeable. Use one consistently throughout your code for better readability.
  • A common convention is to use single quotes unless the string itself contains a single quote, then use double quotes to avoid escaping.
  • Template literals are essential for string interpolation and multiline strings (explained below).

9.2. String Properties

  • length: Returns the number of characters in a string.
const myString = "Hello, World!";
console.log(myString.length); // Output: 13 (including the comma, space, and exclamation mark)

9.3. String Methods

  • charAt(index): Returns the character at the specified index (position) in the string. The index is zero-based (the first character is at index 0).
const str = "JavaScript";
console.log(str.charAt(0)); // Output: J
console.log(str.charAt(4)); // Output: S
console.log(str.charAt(10)); // Output: "" (empty string, because the index is out of bounds)
  • charCodeAt(index): Returns the Unicode (UTF-16) value of the character at the specified index.
const str2 = "JavaScript";
console.log(str2.charCodeAt(0)); // Output: 74 (Unicode value of 'J')
console.log(str2.charCodeAt(4)); // Output: 83 (Unicode value of 'S')
  • substring(startIndex, endIndex): Extracts a part of the string from startIndex (inclusive) to endIndex (exclusive). If endIndex is omitted, it extracts to the end of the string.
const text = "Hello, World!";
console.log(text.substring(0, 5)); // Output: Hello
console.log(text.substring(7)); // Output: World!
console.log(text.substring(7, text.length)); // Output: World!
  • slice(startIndex, endIndex): Similar to substring(), but also accepts negative indices, which count from the end of the string.
const text2 = "Hello, World!";
console.log(text2.slice(0, 5)); // Output: Hello
console.log(text2.slice(7)); // Output: World!
console.log(text2.slice(-6)); // Output: World! (Starts 6 characters from the end)
console.log(text2.slice(-6, -1)); // Output: World (Starts 6 from the end, ends 1 from the end)
  • substr(startIndex, length): (Avoid using this method) Extracts a part of the string starting at startIndex and with a specified length. This method is considered legacy and may be removed in future JavaScript versions. Use substring() or slice() instead.
const text3 = "Hello, World!";
console.log(text3.substr(7, 5)); // Output: World (Works, but avoid using it)

Why avoid substr()?

  1. It’s not part of the core ECMAScript standard.

2. It can behave inconsistently across different JavaScript engines.

3. substring() and slice() provide more consistent and predictable behavior.

  • indexOf(searchValue, fromIndex): Returns the index of the first occurrence of searchValue in the string. Returns -1 if not found. The optional fromIndex argument specifies the index to start the search from.
let sentence = "This is a test sentence.";
console.log(sentence.indexOf("is")); // Output: 2
console.log(sentence.indexOf("is", 3)); // Output: 5 (starts searching from index 3)
console.log(sentence.indexOf("not")); // Output: -1 (not found)
  • lastIndexOf(searchValue, fromIndex): Returns the index of the last occurrence of searchValue in the string. Returns -1 if not found. The optional fromIndex argument specifies the index to start searching backwards from.
let sentence2 = "This is a test sentence. This is another sentence.";
console.log(sentence2.lastIndexOf("is")); // Output: 28
console.log(sentence2.lastIndexOf("is", 20)); // Output: 5 (search backwards from index 20)
  • toUpperCase(): Converts the string to uppercase.
let lowerCase = "hello";
console.log(lowerCase.toUpperCase()); // Output: HELLO
  • toLowerCase(): Converts the string to lowercase.
let upperCase = "WORLD";
console.log(upperCase.toLowerCase()); // Output: world
  • trim(): Removes whitespace from both ends of the string.
let stringWithWhitespace = "   Hello, World!   ";
console.log(stringWithWhitespace.trim()); // Output: "Hello, World!"
  • trimStart() / trimLeft(): (Same function, different names for compatibility) Removes whitespace from the beginning of the string.
let stringWithLeadingWhitespace = "   Hello";
console.log(stringWithLeadingWhitespace.trimStart()); // Output: "Hello"
  • trimEnd() / trimRight(): (Same function, different names for compatibility) Removes whitespace from the end of the string.
let stringWithTrailingWhitespace = "Hello   ";
console.log(stringWithTrailingWhitespace.trimEnd()); // Output: "Hello"
  • replace(searchValue, replaceValue): Replaces the first occurrence of searchValue with replaceValue. searchValue can be a string or a regular expression.
let message = "Hello, World! Hello!";
console.log(message.replace("Hello", "Goodbye")); // Output: Goodbye, World! Hello! (only the first "Hello" is replaced)

// Using a regular expression (case-insensitive)
console.log(message.replace(/hello/i, "Goodbye")); // Output: Goodbye, World! Hello!

// Using a regular expression (replace all, case-insensitive)
console.log(message.replace(/hello/gi, "Goodbye")); // Output: Goodbye, World! Goodbye!
  • replaceAll(searchValue, replaceValue) (ES2021): Replaces all occurrences of searchValue with replaceValue. searchValue can be a string or a regular expression with the global (g) flag.
let message2 = "Hello, World! Hello!";
console.log(message2.replaceAll("Hello", "Goodbye")); // Output: Goodbye, World! Goodbye!

// Using replaceAll with a regular expression (case-insensitive)
console.log(message2.replaceAll(/hello/gi, "Goodbye")); // Output: Goodbye, World! Goodbye!

// Important: If you use replaceAll with a regular expression *without* the global flag (g), it will throw a TypeError.
// console.log(message2.replaceAll(/hello/i, "Goodbye")); // This will cause an error
  • split(separator, limit): Splits the string into an array of substrings, using the specified separator as the delimiter. The optional limit argument specifies the maximum number of elements in the resulting array.
let data = "apple,banana,orange";
console.log(data.split(",")); // Output: [ 'apple', 'banana', 'orange' ]
console.log(data.split(",", 2)); // Output: [ 'apple', 'banana' ] (limit to 2 elements)
console.log(data.split("")); // Output: ['a', 'p', 'p', 'l', 'e', ',', 'b', 'a', 'n', 'a', 'n', 'a', ',', 'o', 'r', 'a', 'n', 'g', 'e'] (splits into individual characters)

9.4. Template Literals (ES6): String Interpolation, Multiline Strings

Template literals provide a more convenient way to create strings, especially when you need to include variables or create multiline strings.

  • String Interpolation: You can embed expressions directly into a string using ${expression}.
let name = "David";
let age = 30;
let greeting = `Hello, my name is ${name} and I am ${age} years old.`;
console.log(greeting); // Output: Hello, my name is David and I am 30 years old.
  • Multiline Strings: Template literals allow you to create strings that span multiple lines without special characters.
let multilineString = `This is a
multiline string
using template literals.`;
console.log(multilineString);
// Output:
// This is a
// multiline string
// using template literals.

Example Combining Concepts:

function formatName(firstName, lastName) {
const fullName = `${firstName} ${lastName}`.trim(); // String interpolation and trim
const formattedName = fullName.charAt(0).toUpperCase() + fullName.slice(1).toLowerCase(); // to capitalize first character of full name and make the rest lower case
return formattedName;
}

console.log(formatName("jOhN", "SMitH")); //Output: John smith

Chapter 10: Object-Oriented Programming (OOP) in JavaScript

10.1. Core OOP Principles

Before we dive into the JavaScript-specific syntax, let’s review the fundamental principles of OOP:

Encapsulation: Bundling data (attributes) and methods (functions) that operate on that data into a single unit (an object). This helps hide internal implementation details and protects the data from direct external access.

Abstraction: Presenting only the essential information about an object to the outside world, hiding complex implementation details. This simplifies the interface and reduces complexity.

Inheritance: Creating new classes (or objects) based on existing classes (or objects), inheriting their properties and methods. This promotes code reuse and reduces redundancy.

  • Polymorphism: The ability of an object to take on many forms. In practice, this means that objects of different classes can respond to the same method call in their own specific ways.

10.2. Prototypal Inheritance

JavaScript uses a prototypal inheritance model, which differs from the classical inheritance model found in languages like Java or C++. In JavaScript, objects inherit properties and methods from other objects through a prototype chain.

Prototypes: Every object in JavaScript has a prototype object associated with it. The prototype object can have its own prototype, and so on, forming a chain. When you try to access a property or method of an object, JavaScript first looks for it directly on the object itself. If it’s not found, it then looks in the object’s prototype, and so on up the prototype chain until it finds the property or method or reaches the end of the chain (where the prototype is null).

  • __proto__ (Deprecated): Every object has a hidden property called __proto__ (double underscore proto double underscore) that points to its prototype. However, this property is considered deprecated and should not be used in production code. It’s primarily for understanding the prototype chain.
  • Object.getPrototypeOf(obj): The standard way to get the prototype of an object.
  • Object.setPrototypeOf(obj, prototype): The standard way to set the prototype of an object. Use with caution, as changing an object’s prototype after it’s created can have performance implications.
// Creating a simple object
const myObject = {
name: "My Object",
greet: function() {
console.log("Hello from " + this.name);
}
};

// Accessing the prototype
const prototype = Object.getPrototypeOf(myObject);
console.log(prototype); // Output: [Object: null prototype] {} (the default prototype for objects)

// Checking if a property exists on the object vs. its prototype
console.log(myObject.hasOwnProperty("name")); // Output: true (property is directly on the object)
console.log(myObject.hasOwnProperty("toString")); // Output: false (toString is inherited from the prototype)

// Example of setting a new prototype:

const newPrototype = {
sayHi: function() {
console.log("Hi from the new prototype!");
}
};

Object.setPrototypeOf(myObject, newPrototype);

myObject.sayHi(); // Output: Hi from the new prototype!
myObject.greet(); //error myObject.greet is not a function

10.3. ES5 constructor function to create class

Constructor functions are used to create objects of a specific “type” (like a class in other languages). They are regular JavaScript functions that are called using the new keyword.

// Constructor function
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
}

new Operator:

  • Creates a new, empty object.
  • Sets the this value inside the constructor function to point to the newly created object.
  • Sets the prototype of the new object to be the prototype property of the constructor function.
  • Executes the constructor function.
  • If the constructor function doesn’t explicitly return an object, it returns the newly created object.
// Creating objects using the new keyword
const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);

person1.greet(); // Output: Hello, my name is Alice and I am 30 years old.
person2.greet(); // Output: Hello, my name is Bob and I am 25 years old.

console.log(person1 instanceof Person); // Output: true
console.log(person2 instanceof Object); // Output: true (all objects inherit from Object)

10.4. The Prototype Chain

When you create an object using a constructor function, the new object’s prototype is set to ConstructorFunction.prototype. This is where you can add methods that will be shared by all instances of the constructor function.

function Animal(name) {
this.name = name;
}

// Adding a method to the Animal prototype
Animal.prototype.sayName = function() {
console.log("My name is " + this.name);
};

const dog = new Animal("Buddy");
dog.sayName(); // Output: My name is Buddy

// Adding a new method to the Object prototype (be cautious about modifying built-in prototypes)
Object.prototype.logPrototype = function() {
console.log("Object Prototype Method");
}

dog.logPrototype(); // this will work.
//Checking if the prototypes are linked
console.log(Object.getPrototypeOf(dog) === Animal.prototype); //true

//Let's modify the prototype
Animal.prototype.type = "Animal"; //adding the animal type to Animal prototype
console.log(dog.type); //dog object can access the type

const cat = new Animal("Whiskers"); //creating new animal
console.log(cat.type); //this also has the animal type
  • Modifying built-in object prototypes (like Object.prototype, Array.prototype, etc.) is generally discouraged because it can lead to unexpected behavior and conflicts with other libraries or code.

10.5. Object.create()

Object.create() creates a new object with the specified prototype object and properties. It provides more control over the prototype chain.

const animalPrototype = {
sayName: function() {
console.log("My name is " + this.name);
}
};

const lion = Object.create(animalPrototype);
lion.name = "Simba";
lion.sayName(); // Output: My name is Simba

console.log(Object.getPrototypeOf(lion) === animalPrototype); // Output: true

10.6. ES6 Classes

ES6 (ECMAScript 2015) introduced the class keyword, providing a more familiar syntax for defining object blueprints, similar to classes in other languages. It’s important to remember that ES6 classes are still based on JavaScript’s prototypal inheritance model; they are syntactic sugar over the existing prototype-based system.

class: Defines a class (blueprint for creating objects).

class Vehicle {
constructor(make, model) {
this.make = make;
this.model = model;
}

startEngine() {
console.log("Engine started");
}
}
  • constructor: A special method within the class that is called when a new object is created using the new keyword. It’s used to initialize the object’s properties.
  • extends: Used to create a subclass (child class) that inherits from a superclass (parent class).
class Car extends Vehicle {
constructor(make, model, numDoors) {
super(make, model); // Call the superclass constructor
this.numDoors = numDoors;
}

honk() {
console.log("Honk!");
}

startEngine() {
super.startEngine(); //call to start engine of parent
console.log("Car engine started"); //Overriding
}
}
  • super: Used within a subclass to call the constructor or methods of the superclass.
  • static: Defines static methods or properties that belong to the class itself, rather than to instances of the class.
class Vehicle {
constructor(make, model) {
this.make = make;
this.model = model;
}
static getDefaultColor() {
return "White";
}
}
  • Getters and Setters: Special methods that allow you to control access to object properties.
class Vehicle {
constructor(make, model) {
this.make = make;
this.model = model;
}

get description() { //Getter method
return `${this.make} ${this.model}`;
}

set color(newColor){ //Setter method
this._color = newColor;
}
}

Following is full code to show the class example.

class Vehicle {
constructor(make, model) {
this.make = make;
this.model = model;
}

startEngine() {
console.log("Engine started");
}

static getDefaultColor() {
return "White";
}

get description() { //Getter method
return `${this.make} ${this.model}`;
}

set color(newColor){ //Setter method
this._color = newColor;
}
}

const myCar = new Vehicle("Toyota", "Camry");
myCar.startEngine(); // Output: Engine started
console.log(Vehicle.getDefaultColor()); // Output: White

class Car extends Vehicle {
constructor(make, model, numDoors) {
super(make, model); // Call the superclass constructor
this.numDoors = numDoors;
}

honk() {
console.log("Honk!");
}

startEngine() {
super.startEngine(); //call to start engine of parent
console.log("Car engine started"); //Overriding
}
}

const myCar2 = new Car("Honda", "Civic", 4);
myCar2.startEngine();
//Output:
//Engine started
//Car engine started

myCar2.honk(); // Output: Honk!

console.log(myCar2.description); //accessing the getter of the Car class

myCar2.color = "red"; //setting the color using the setter
console.log(myCar2._color); //Output: red

10.7. Static Methods

Static methods are associated with the class itself, not with instances of the class. You call them directly on the class, not on objects created from the class. They’re often used for utility functions that are related to the class but don’t need to access instance-specific data.

class MathUtils {
static add(a, b) {
return a + b;
}
}

console.log(MathUtils.add(5, 3)); // Output: 8 (called directly on the class)

const math = new MathUtils(); //Cannot directly call the static method from object
//math.add(4,5); //causes error

10.8. Getters and Setters

Getters and setters are special methods that allow you to control how object properties are accessed and modified. They provide a way to encapsulate the internal representation of an object and enforce certain rules or validation when properties are read or written.

class Circle {
constructor(radius) {
this._radius = radius; // Use an underscore to indicate a "private" property (convention only)
}

get radius() { //getter method
return this._radius;
}

set radius(value) {
if (value > 0) {
this._radius = value;
} else {
console.error("Radius must be positive");
}
}

get area() { //getter method
return Math.PI * this._radius * this._radius;
}
}

const myCircle = new Circle(5);
console.log(myCircle.radius); // Output: 5 (accessing the getter)

myCircle.radius = 10; //setting using the setter method

console.log(myCircle.radius); // Output: 10

myCircle.radius = -1; // Output: Radius must be positive (setter validation)

console.log(myCircle.area); // Output: 314.159... (calculated using the getter)

10.9. Private Fields and Methods (ES2022)

JavaScript has a new syntax for declaring truly private fields and methods within classes, using the # prefix. Private fields are only accessible from within the class itself, providing stronger encapsulation.

class Counter {
#count = 0; // Private field

increment() {
this.#count++;
}

getCount() {
return this.#count; // Accessing private field within the class
}

#logCount() { // Private method
console.log("Current count:", this.#count);
}

accessLogCount(){ //public method that uses private method
this.#logCount();
}
}

const myCounter = new Counter();
myCounter.increment();
console.log(myCounter.getCount()); // Output: 1

// console.log(myCounter.#count); // Error: Private field '#count' must be declared in an enclosing class
// myCounter.#logCount(); //error : Private method must be declared in an enclosing class

myCounter.accessLogCount();

10.10. Encapsulation, Inheritance, Polymorphism in Practice

Let’s illustrate these principles with a more comprehensive example:

// Encapsulation: The internal properties (speed, color) are managed within the class.
// Abstraction: The user only interacts with the public methods (accelerate, brake).
class Vehicle {
constructor(make, model) {
this.make = make;
this.model = model;
this.speed = 0;
}

accelerate(increment) {
this.speed += increment;
console.log(`Accelerating to ${this.speed} mph`);
}

brake(decrement) {
this.speed -= decrement;
if (this.speed < 0) {
this.speed = 0;
}
console.log(`Braking to ${this.speed} mph`);
}

getDescription() {
return `${this.make} ${this.model}. Current speed: ${this.speed} mph`;
}
}

// Inheritance: Car inherits from Vehicle, extending its functionality.
class Car extends Vehicle {
constructor(make, model, numDoors) {
super(make, model);
this.numDoors = numDoors;
}

honk() {
console.log("Honk!");
}

getDescription() { //overriding
return `${super.getDescription()} . Number of doors: ${this.numDoors}`;
}
}

// Polymorphism: The getDescription() method behaves differently depending on the object type.
const genericVehicle = new Vehicle("Generic", "Vehicle");
const myCar = new Car("Honda", "Civic", 4);

console.log(genericVehicle.getDescription()); // Output: Generic Vehicle. Current speed: 0 mph
console.log(myCar.getDescription()); // Output: Honda Civic. Current speed: 0 mph. Number of doors: 4

Chapter 11: Asynchronous JavaScript: Callbacks, Promises, and Async/Await

11.1 Synchronous operations

JavaScript is single-threaded, which means it can only execute one task at a time. In synchronous, code executes line by line, in order. Each operation waits for the previous one to complete.

11.2 Asynchronous Operations

Many operations (like fetching data from a server or reading a file) can take a considerable amount of time. If JavaScript were to wait synchronously for these operations to complete, it would block the main thread, causing the user interface to freeze and become unresponsive. This is where asynchronous programming comes in.

Asynchronous programming allows JavaScript to start an operation and then continue executing other code without waiting for the operation to finish. When the operation is complete, JavaScript is notified, and it can then execute a callback function, resolve a promise, or continue execution in an async function.

11.3. The JavaScript Event Loop

The event loop is a fundamental part of how JavaScript handles asynchronous operations. Here’s a simplified overview:

  1. Call Stack: This is where JavaScript executes synchronous code. When a function is called, it’s added to the call stack. When the function completes, it’s removed from the call stack.
  2. Web APIs (or Browser APIs): These are features provided by the browser (or Node.js environment) for performing asynchronous operations like setTimeout, fetch, DOM manipulation, etc.
  3. Task Queue (or Callback Queue): When an asynchronous operation in the Web APIs completes, a task (typically a callback function) is added to the task queue.
  4. Microtask Queue: Stores microtasks (e.g., promises, MutationObserver callbacks), which have higher priority than regular tasks. Microtasks are executed before the event loop processes the next task from the task queue.
  5. Event Loop: The event loop continuously monitors the call stack and the task queue. If the call stack is empty, it takes the first task from the task queue and pushes it onto the call stack for execution.

11.4. Asynchronous programming using Callbacks (Old way)

function fetchData(callback) {
// Simulate an asynchronous operation (e.g., fetching data from a server)
setTimeout(() => {
const data = { name: "Alice", age: 30 };
callback(data); // Call the callback function with the data
}, 1000); // Simulate a 1-second delay
}

function processData(data) {
console.log("Data received:", data);
}

fetchData(processData); // Pass processData as a callback to fetchData
console.log("Fetching data..."); // This will execute before the data is received

Callbacks are functions that are passed as arguments to other functions and are executed when the asynchronous operation completes. They are the traditional way of handling asynchronous operations in JavaScript.

11.5 Callback Hell Problem(Pyramid of Doom)

Nested callbacks can lead to a situation known as “callback hell,” where the code becomes deeply indented and difficult to read and maintain. This often occurs when multiple asynchronous operations depend on each other.

// Example of callback hell
function doTask1(callback) {
setTimeout(() => {
console.log("Task 1 completed");
callback();
}, 500);
}

function doTask2(callback) {
setTimeout(() => {
console.log("Task 2 completed");
callback();
}, 500);
}

function doTask3(callback) {
setTimeout(() => {
console.log("Task 3 completed");
callback();
}, 500);
}

doTask1(() => {
doTask2(() => {
doTask3(() => {
console.log("All tasks completed");
});
});
});

Callback hell makes it hard to reason about the flow of execution, handle errors, and maintain the code. Promises and async/await provide more elegant solutions to these problems.

11.6. Promises to write Asyncrouns code

Promises are objects that represent the eventual completion (or failure) of an asynchronous operation and its resulting value. They provide a cleaner and more structured way to handle asynchronous code compared to callbacks.

A promise has three states:

  • Pending: The initial state; the operation is still in progress.
  • Fulfilled (Resolved): The operation completed successfully, and the promise has a value.
  • Rejected: The operation failed, and the promise has a reason for the failure.

Creating a Promise:

const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation (e.g., fetching data)
setTimeout(() => {
const success = true; // Simulate success or failure

if (success) {
const data = { name: "David", age: 35 };
resolve(data); // Resolve the promise with the data
} else {
const error = new Error("Failed to fetch data");
reject(error); // Reject the promise with an error
}
}, 1000);
});

Using a Promise (then, catch, finally):

  • then(onFulfilled, onRejected): Registers callback functions to be executed when the promise is either fulfilled or rejected. It returns a new promise, allowing for chaining.
  • catch(onRejected): Registers a callback function to be executed when the promise is rejected. It’s equivalent to calling then(null, onRejected). It also returns a new promise, allowing for chaining.
  • finally(onFinally): Registers a callback function to be executed regardless of whether the promise is fulfilled or rejected. It’s often used for cleanup tasks (e.g., closing a connection, hiding a loading indicator). finally does not receive any arguments and does not affect the state of the promise.
myPromise
.then(data => {
console.log("Data received:", data);
return data.age; // You can return a value to be used in the next .then
})
.then(age => {
console.log("Age:", age);
})
.catch(error => {
console.error("Error:", error.message);
})
.finally(() => {
console.log("Promise completed");
});

console.log("Promise initiated..."); // This will execute before the promise resolves or rejects

Explanation:

  1. myPromise is created and will eventually resolve or reject.
  2. The first then block is executed if the promise resolves. It receives the resolved data and logs it to the console. It returns the age, which is passed to the next then block.
  3. The second then block receives the age and logs it.
  4. The catch block is executed if the promise rejects. It receives the error and logs its message.
  5. The finally block is executed regardless of whether the promise resolves or rejects. It logs “Promise completed”.
  6. “Promise initiated…” is logged to the console before the promise resolves or rejects because the promise is asynchronous.

Promise Chaining:

Promises allow you to chain asynchronous operations together in a sequential manner, avoiding callback hell.

function fetchData2(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // Simulate success/failure based on URL
if (success) {
const data = `Data from ${url}`;
resolve(data);
} else {
reject(new Error(`Failed to fetch from ${url}`));
}
}, 500);
});
}

fetchData2("url1")
.then(data1 => {
console.log(data1); // Process data from url1
return fetchData2("url2"); // Fetch data from url2
})
.then(data2 => {
console.log(data2); // Process data from url2
return fetchData2("url3"); // Fetch data from url3
})
.then(data3 => {
console.log(data3); // Process data from url3
})
.catch(error => {
console.error("Error:", error.message);
});

Promise.all(), Promise.race(), Promise.allSettled(), Promise.any()

These methods allow you to work with multiple promises concurrently.

  • Promise.all(iterable): Takes an iterable (e.g., an array) of promises as input. It returns a single promise that resolves when all of the input promises have resolved. The resolved value is an array containing the resolved values of the input promises in the same order. If any of the input promises reject, the Promise.all() promise immediately rejects with the reason of the first rejected promise.
const promise1 = Promise.resolve(1);
const promise2 = new Promise(resolve => setTimeout(() => resolve(2), 200));
const promise3 = new Promise((resolve, reject) => setTimeout(() => reject("Error!"), 100));

Promise.all([promise1, promise2]).then(values => {
console.log("All promises resolved:", values); // Output: All promises resolved: [1, 2]
}).catch(error => {
console.error("Promise.all error:", error);
});

Promise.all([promise1, promise2, promise3]).then(values => { //This promise will reject
console.log("All promises resolved:", values);
}).catch(error => {
console.error("Promise.all error:", error); //Output: Promise.all error: Error!
});
  • Promise.race(iterable): Takes an iterable of promises as input. It returns a single promise that resolves or rejects as soon as any of the input promises resolves or rejects. The resolved or rejected value is the same as the first promise to resolve or reject.
const promiseA = new Promise(resolve => setTimeout(() => resolve("A"), 100));
const promiseB = new Promise(reject => setTimeout(() => reject("B"), 50));

Promise.race([promiseA, promiseB]).then(value => {
console.log("Promise.race resolved:", value);
}).catch(reason => {
console.error("Promise.race rejected:", reason); // Output: Promise.race rejected: B (because promiseB rejected first)
});
  • Promise.allSettled(iterable): Takes an iterable of promises as input. It returns a single promise that resolves when all of the input promises have settled (either resolved or rejected). The resolved value is an array of objects, each describing the outcome of the corresponding promise. Each object has a status property (either “fulfilled” or “rejected”) and a value property (if fulfilled) or a reason property (if rejected). This is useful when you want to know the outcome of all promises, even if some of them reject.
const promiseX = Promise.resolve("X");
const promiseY = new Promise(resolve => setTimeout(() => resolve("Y"), 100));
const promiseZ = new Promise(reject => setTimeout(() => reject("Z"), 50));

Promise.allSettled([promiseX, promiseY, promiseZ]).then(results => {
console.log("Promise.allSettled results:", results);
// Output:
// Promise.allSettled results: [
// { status: 'fulfilled', value: 'X' },
// { status: 'fulfilled', value: 'Y' },
// { status: 'rejected', reason: 'Z' }
// ]
});
  • Promise.any(iterable): Takes an iterable of promises as input. It returns a single promise that resolves as soon as any of the input promises resolves. If all of the input promises reject, the Promise.any() promise rejects with an AggregateError containing all of the rejection reasons.
const promiseOne = Promise.reject("One");
const promiseTwo = new Promise((resolve, reject) => setTimeout(() => reject("Two"), 50));
const promiseThree = new Promise(resolve => setTimeout(() => resolve("Three"), 100));

Promise.any([promiseOne, promiseTwo, promiseThree]).then(value => {
console.log("Promise.any resolved:", value); // Output: Promise.any resolved: Three
}).catch(error => {
console.error("Promise.any rejected:", error);
});

const promiseFour = Promise.reject("Four");
const promiseFive = new Promise((resolve, reject) => setTimeout(() => reject("Five"), 50));
Promise.any([promiseFour, promiseFive]).then(value => {
console.log("Promise.any resolved:", value);
}).catch(error => {
console.error("Promise.any rejected:", error); //AggregateError [All Promises rejected]
});

11.7 Async/Await (ES2017) to write Asynchronous code

async/await is syntactic sugar built on top of promises. It makes asynchronous code look and behave a bit more like synchronous code, which can improve readability and maintainability.

  • async: A keyword that is placed before a function declaration to indicate that the function is asynchronous. async functions implicitly return a promise. If the function returns a value, the promise will resolve with that value. If the function throws an error, the promise will reject with that error.
async function fetchDataAsync() {
}
  • await: A keyword that can only be used inside async functions. It pauses the execution of the async function until a promise is resolved or rejected. It then returns the resolved value of the promise, or throws an error if the promise is rejected.
async function fetchDataAsync() {
const count = await readDatabase();
}

Example

(async function() {
async readData() {
// Make database call
try {
const data = await readDatabase();
} catch(err) {
throw err;
}
return data;
}
const result = readData();
console.log({result});
})()

Benefits of Async/Await:

  • Improved Readability: async/await makes asynchronous code look more like synchronous code, making it easier to read and understand.
  • Simplified Error Handling: You can use try…catch blocks to handle errors in asynchronous code, just like you would in synchronous code.
  • Easier Debugging: Debugging asynchronous code with async/await is easier than debugging callback-based code because you can step through the code line by line in a debugger.

Example Combining Promises and Async/Await:

async function fetchAndProcessData(url1, url2) {
try {
const data1 = await fetchData2(url1); // Uses the promise function from before
console.log("Received Data1:", data1);

const data2 = await fetchData2(url2);
console.log("Received Data2:", data2);

return `Combined data: ${data1} and ${data2}`;

} catch (error) {
console.error("Error:", error.message);
throw error; // Re-throw the error to be caught by a higher-level handler
}
}

fetchAndProcessData("urlA", "urlB")
.then(combinedData => {
console.log("Combined Data:", combinedData);
})
.catch(error => {
console.error("Final Error Handler:", error.message);
});

11.8 Error Propagation in async/await Functions

In async/await functions, error propagation behaves similarly to how it works in synchronous code. However, because async/await deals with promises and asynchronous operations, there are some nuances to consider.

1. try…catch Blocks

The most common and effective way to handle errors in async/await functions is to use try…catch blocks. The try block encloses the code that might throw an error, and the catch block handles any errors that are thrown within the try block.

async function fetchData(url) {
try {
// Simulate an asynchronous operation that might fail
const response = await new Promise((resolve, reject) => {
setTimeout(() => {
const success = url === "https://example.com/data"; // Simulate success for a specific URL
if (success) {
resolve({ data: "Data from the server" });
} else {
reject(new Error(`Failed to fetch data from ${url}`));
}
}, 500);
});

console.log("Data fetched:", response.data);
return response.data;

} catch (error) {
console.error("Error in fetchData:", error.message);
// You can choose to handle the error here, or re-throw it to be handled elsewhere
throw error; // Re-throw the error to propagate it up the call stack
}
}

async function processData() {
try {
const data = await fetchData("https://example.com/data"); // this will work
//const data = await fetchData("https://bad-example.com/data"); //This will fail
console.log("Processed data:", data);
} catch (error) {
console.error("Error in processData:", error.message);
}
}

processData();

2. Error Propagation Up the Call Stack

If you re-throw an error inside the catch block of an async function, the error will be propagated up the call stack to the calling function. If the calling function is also an async function, it can use a try…catch block to handle the error. If the calling function is not an async function, you’ll need to use a then().catch() block to handle the rejection of the promise returned by the async function.

Let’s modify the previous example to demonstrate error propagation:

async function fetchData(url) {
try {
// Simulate an asynchronous operation that might fail
const response = await new Promise((resolve, reject) => {
setTimeout(() => {
const success = url === "https://example.com/data"; // Simulate success for a specific URL
if (success) {
resolve({ data: "Data from the server" });
} else {
reject(new Error(`Failed to fetch data from ${url}`));
}
}, 500);
});

console.log("Data fetched:", response.data);
return response.data;

} catch (error) {
console.error("Error in fetchData:", error.message);
throw error; // Re-throw the error to propagate it up the call stack
}
}

async function processData() {
try {
const data = await fetchData("https://bad-example.com/data"); // This will fail
console.log("Processed data:", data);
} catch (error) {
console.error("Error in processData:", error.message);
// Handle the error here (e.g., display an error message to the user)
}
}

processData();

3. Handling Errors in Non-Async Functions

If you call an async function from a non-async function, you need to use a then().catch() block to handle the promise returned by the async function.

async function fetchData(url) {
try {
// Simulate an asynchronous operation that might fail
const response = await new Promise((resolve, reject) => {
setTimeout(() => {
const success = url === "https://example.com/data"; // Simulate success for a specific URL
if (success) {
resolve({ data: "Data from the server" });
} else {
reject(new Error(`Failed to fetch data from ${url}`));
}
}, 500);
});

console.log("Data fetched:", response.data);
return response.data;

} catch (error) {
console.error("Error in fetchData:", error.message);
throw error; // Re-throw the error to propagate it up the call stack
}
}

function startFetching() {
fetchData("https://bad-example.com/data")
.then(data => {
console.log("Data from startFetching:", data);
})
.catch(error => {
console.error("Error in startFetching:", error.message);
});
}

startFetching();

4. Using finally for Cleanup

The finally block can be used to execute code regardless of whether the promise resolves or rejects. This is often useful for cleanup tasks, such as hiding a loading indicator, closing a connection, or releasing resources.

async function fetchData(url) {
try {
// Simulate an asynchronous operation that might fail
const response = await new Promise((resolve, reject) => {
setTimeout(() => {
const success = url === "https://example.com/data"; // Simulate success for a specific URL
if (success) {
resolve({ data: "Data from the server" });
} else {
reject(new Error(`Failed to fetch data from ${url}`));
}
}, 500);
});

console.log("Data fetched:", response.data);
return response.data;

} catch (error) {
console.error("Error in fetchData:", error.message);
throw error; // Re-throw the error to propagate it up the call stack
} finally {
console.log("Fetch operation completed (regardless of success or failure)");
}
}

async function processData() {
try {
const data = await fetchData("https://bad-example.com/data"); // This will fail
console.log("Processed data:", data);
} catch (error) {
console.error("Error in processData:", error.message);
// Handle the error here (e.g., display an error message to the user)
}
}

processData();

5. Throwing Custom Errors

You can throw custom errors in your async functions to provide more specific information about the error that occurred. This can be helpful for debugging and for handling errors in a more granular way.

class CustomError extends Error {
constructor(message, code) {
super(message);
this.code = code;
this.name = "CustomError";
}
}

async function fetchData(url) {
try {
// Simulate an asynchronous operation that might fail
const response = await new Promise((resolve, reject) => {
setTimeout(() => {
const success = url === "https://example.com/data"; // Simulate success for a specific URL
if (success) {
resolve({ data: "Data from the server" });
} else {
reject(new CustomError(`Failed to fetch data from ${url}`, 500));
}
}, 500);
});

console.log("Data fetched:", response.data);
return response.data;

} catch (error) {
console.error("Error in fetchData:", error.message, error.code); // we get custom error code
throw error; // Re-throw the error to propagate it up the call stack
} finally {
console.log("Fetch operation completed (regardless of success or failure)");
}
}

Chapter 12: Date

The Date object represents a single moment in time in a platform-independent format. It essentially stores the number of milliseconds that have elapsed since the Unix epoch (January 1, 1970, 00:00:00 Coordinated Universal Time (UTC)).

12.1 Creating Date Objects

  • new Date(): Creates a new Date object representing the current date and time.
const now = new Date();
console.log(now); // Example: 2023-11-06T14:30:00.000Z (This will vary based on your current time)
  • new Date(milliseconds): Creates a new Date object representing the time that is milliseconds after the Unix epoch.
const epochPlusOneSecond = new Date(1000); // 1000 milliseconds = 1 second
console.log(epochPlusOneSecond); // Example: 1970-01-01T00:00:01.000Z
  • new Date(dateString): Creates a new Date object from a date string. The format of the date string should be one that is recognized by the Date.parse() method.
const dateFromString = new Date("December 17, 1995 03:24:00");
console.log(dateFromString); // Example: 1995-12-17T03:24:00.000Z
  • new Date(year, monthIndex, day, hours, minutes, seconds, milliseconds): Creates a new Date object with the specified date and time components.

year: The year (four digits).

monthIndex: The month (0–11, where 0 is January and 11 is December).

day: The day of the month (1–31).

hours: The hour (0–23).

minutes: The minute (0–59).

seconds: The second (0–59).

milliseconds: The millisecond (0–999).

const specificDate = new Date(2023, 10, 6, 15, 0, 0, 0); // November 6, 2023, 3:00 PM
console.log(specificDate); // Example: 2023-11-06T15:00:00.000Z

12.2 Date Get Methods

These methods allow you to retrieve various components of a Date object.

  • getFullYear(): Returns the year (four digits).
const year = now.getFullYear();
console.log(year); // Example: 2023
  • getMonth(): Returns the month (0–11).
const month = now.getMonth();
console.log(month); // Example: 10 (November)
  • getDate(): Returns the day of the month (1–31).
const dayOfMonth = now.getDate();
console.log(dayOfMonth); // Example: 6
  • getDay(): Returns the day of the week (0–6, where 0 is Sunday and 6 is Saturday).
const dayOfWeek = now.getDay();
console.log(dayOfWeek); // Example: 1 (Monday)
  • getHours(): Returns the hour (0–23).
const hours = now.getHours();
console.log(hours); // Example: 14
  • getMinutes(): Returns the minute (0–59).
const minutes = now.getMinutes();
console.log(minutes); // Example: 30
  • getSeconds(): Returns the second (0–59).
const seconds = now.getSeconds();
console.log(seconds); // Example: 0
  • getMilliseconds(): Returns the millisecond (0–999).
const milliseconds = now.getMilliseconds();
console.log(milliseconds); // Example: 0
  • getTime(): Returns the number of milliseconds since the Unix epoch.
const timeInMilliseconds = now.getTime();
console.log(timeInMilliseconds); // Example: 1699271400000
  • getTimezoneOffset(): Returns the difference between UTC and local time, in minutes.
const timezoneOffset = now.getTimezoneOffset();
console.log(timezoneOffset); // Example: -300 (Eastern Time)
  • getUTCDate(), getUTCFullYear(), getUTCHours(), etc.: These methods are similar to the non-UTC methods, but they return the corresponding date and time components in Coordinated Universal Time (UTC).
const utcDate = now.getUTCDate();
console.log(utcDate); // Example: 6 (UTC date)

12.3. Date Set Methods

These methods allow you to modify various components of a Date object.

  • setFullYear(year, monthIndex, day): Sets the year, month, and day.
now.setFullYear(2024); 
console.log(now); // Example: 2024-11-06T14:30:00.000Z
  • setMonth(monthIndex, day): Sets the month and day.
now.setMonth(11); // December (11) 
console.log(now); // Example: 2024-12-06T14:30:00.000Z
  • setDate(day): Sets the day of the month.
now.setDate(25); 
console.log(now); // Example: 2024-12-25T14:30:00.000Z
  • setHours(hours, minutes, seconds, milliseconds): Sets the hour, minute, second, and millisecond.
now.setHours(16, 30, 0, 0); 
console.log(now); // Example: 2024-12-25T16:30:00.000Z
  • setMinutes(minutes, seconds, milliseconds): Sets the minute, second, and millisecond.
now.setMinutes(45, 0, 0); 
console.log(now); // Example: 2024-12-25T16:45:00.000Z
  • setSeconds(seconds, milliseconds): Sets the second and millisecond.
now.setSeconds(30, 0); 
console.log(now); // Example: 2024-12-25T16:45:30.000Z
  • setMilliseconds(milliseconds): Sets the millisecond.
now.setMilliseconds(500); 
console.log(now); // Example: 2024-12-25T16:45:30.500Z
  • setTime(milliseconds): Sets the Date object to the time represented by milliseconds after the Unix epoch.
now.setTime(0); // Sets the date to the Unix epoch console.log(now); // Example: 1970-01-01T00:00:00.000Z
  • setUTCDate(), setUTCFullYear(), setUTCHours(), etc.: These methods are similar to the non-UTC methods, but they set the corresponding date and time components in Coordinated Universal Time (UTC).

12.4. Date Conversion Methods

These methods allow you to convert a Date object to a string representation in various formats.

  • toString(): Returns a string representation of the Date object in a human-readable format (implementation-dependent).
const now = new Date();
console.log(now.toString()); // Example: Wed Dec 25 2024 16:45:30 GMT-0500 (Eastern Standard Time)
  • toDateString(): Returns the date portion of the Date object as a human-readable string.
const now = new Date();
console.log(now.toDateString()); // Example: Wed Dec 25 2024
  • toTimeString(): Returns the time portion of the Date object as a human-readable string.
const now = new Date();
console.log(now.toTimeString()); // Example: 16:45:30 GMT-0500 (Eastern Standard Time)
  • toISOString(): Returns a string representation of the Date object in the ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ).
const now = new Date();
console.log(now.toISOString()); // Example: 2024-12-25T21:45:30.500Z (UTC)
  • toLocaleString(): Returns a string representation of the Date object in a locale-specific format.
const now = new Date();
console.log(now.toLocaleString()); // Example: 12/25/2024, 4:45:30 PM (US English)
  • toLocaleDateString(): Returns the date portion of the Date object in a locale-specific format.
const now = new Date();
console.log(now.toLocaleDateString()); // Example: 12/25/2024 (US English)
  • toLocaleTimeString(): Returns the time portion of the Date object in a locale-specific format.
const now = new Date();
console.log(now.toLocaleTimeString()); // Example: 4:45:30 PM (US English)
  • toUTCString(): Returns a string representation of the Date object in UTC.
const now = new Date();
console.log(now.toUTCString()); // Example: Wed, 25 Dec 2024 21:45:30 GMT
  • valueOf(): Returns the primitive value of a Date object as the number of milliseconds since midnight January 1, 1970 UTC.
const now = new Date();
console.log(now.valueOf()); //Similar to the getTime() function

12.5. Date.parse()

The Date.parse() method parses a string representation of a date and returns the number of milliseconds since the Unix epoch. It’s used internally by the new Date(dateString) constructor.

const millisecondsSinceEpoch = Date.parse("March 21, 2012");
console.log(millisecondsSinceEpoch); // Example: 1332302400000

12.6. Date.now()

The Date.now() method returns the number of milliseconds that have elapsed since the Unix epoch. It is a static method, meaning it’s called on the Date object itself, not on an instance of the Date object.

const currentMilliseconds = Date.now();
console.log(currentMilliseconds); // Example: 1699271400000 (This will vary based on the current time)

12.7. Date Comparison

You can compare Date objects using comparison operators (<, >, <=, >=, ==, !=). However, it’s generally recommended to compare the getTime() values of the Date objects to ensure accurate comparisons.

const date1 = new Date(2023, 0, 1); // January 1, 2023
const date2 = new Date(2023, 0, 15); // January 15, 2023
console.log(date1 < date2); // true (but rely on .getTime() for reliable comparisons)
console.log(date1.getTime() < date2.getTime()); // More reliable: true

12.8. Working with Time Zones

JavaScript’s Date object has limited support for time zones. It uses the local time zone of the user’s system by default. To work with specific time zones, you’ll typically need to use a library like Moment.js Timezone or Luxon. These libraries provide more advanced features for time zone conversions and handling daylight saving time.

12.9. Formatting Dates

The built-in Date object doesn’t provide extensive formatting options. For more flexible date formatting, consider using a library like Moment.js, date-fns, or Luxon.

Example Using Luxon for Date Formatting:

//Luxon needs to be included in your project to be used
const { DateTime } = luxon;
const nowLuxon = DateTime.now();
console.log(nowLuxon.toLocaleString(DateTime.DATE_FULL)); // December 25, 2024
console.log(nowLuxon.toFormat('yyyy-MM-dd HH:mm:ss')); // 2024-12-25 16:45:30

This post is based on interaction with https://aistudio.google.com/

Happy learning :-)

--

--

Dilip Kumar
Dilip Kumar

Written by Dilip Kumar

With 18+ years of experience as a software engineer. Enjoy teaching, writing, leading team. Last 4+ years, working at Google as a backend Software Engineer.

Responses (11)