Javascript - Beginner to Advance


The Complete Beginner’s Guide to JavaScript

Welcome to the exciting world of JavaScript! This document is designed to be your comprehensive guide, taking you from a complete novice to a confident JavaScript developer. We’ll cover everything from the absolute basics to advanced topics and practical projects, all explained in a clear, simple, and logical manner.


1. Introduction to Javascript

What is Javascript?

JavaScript (often abbreviated as JS) is a powerful, high-level, and incredibly versatile programming language. It’s primarily known as the scripting language for web pages, allowing you to implement complex features on web pages. When you see a webpage that does more than just display static information—displaying timely content updates, interactive maps, animated 2D/3D graphics, scrolling video jukeboxes, etc.—you can bet that JavaScript is involved. It’s one of the three core technologies of the World Wide Web, alongside HTML and CSS.

Beyond the browser, JavaScript has expanded its reach significantly with technologies like Node.js, enabling server-side programming, and frameworks like React Native, for mobile app development.

Why learn Javascript? (Benefits, use cases, industry relevance)

Learning JavaScript offers a multitude of benefits and opens up a vast array of opportunities:

  • Ubiquity: JavaScript runs virtually everywhere. If you’re building for the web, JavaScript is essential. With Node.js, you can also use it for backend development, making it a full-stack language.
  • High Demand: The demand for JavaScript developers remains consistently high across industries due to its widespread use in web development, mobile app development, and even desktop applications.
  • Versatility: You can build almost anything with JavaScript:
    • Interactive Websites: The most common use case.
    • Web Applications: Single-page applications (SPAs) like Gmail, Trello.
    • Backend Services: With Node.js, build powerful and scalable server-side applications.
    • Mobile Applications: Using frameworks like React Native and Expo.
    • Desktop Applications: With Electron (e.g., VS Code, Slack).
    • Games: Web-based games and even more complex ones using libraries like Phaser.
    • AI/Machine Learning (Client-side): Newer advancements allow for on-device AI capabilities using Chrome’s built-in AI APIs (e.g., Prompt API, Summarizer API, Language Detector API) powered by models like Gemini Nano.
  • Large Ecosystem and Community: JavaScript boasts an enormous ecosystem of libraries, frameworks, and tools. This means you’ll rarely need to build things from scratch, and you’ll always find solutions and support from its massive, active community.
  • Career Opportunities: Proficiency in JavaScript is a critical skill for roles like Front-end Developer, Back-end Developer (Node.js), Full-stack Developer, Mobile App Developer, and UI/UX Engineer.

A brief history (optional, keep it concise)

JavaScript was created in 1995 by Brendan Eich while he was working at Netscape. It was initially named LiveScript but was quickly renamed JavaScript, largely due to the popularity of Java at the time, even though the languages are quite different. Its purpose was to add interactivity to web pages. Over the years, it has evolved significantly, with new standards (ECMAScript) being released annually, adding powerful features and capabilities that have cemented its place as a cornerstone of modern software development.

Setting up your development environment

To start writing and running JavaScript, you’ll need a few things:

  1. A Web Browser: Modern web browsers (like Google Chrome, Mozilla Firefox, Microsoft Edge, Safari) come with a built-in JavaScript engine. We’ll use Chrome’s developer tools frequently.
  2. A Text Editor: You’ll need a program to write your code. Popular choices include:
    • Visual Studio Code (VS Code): Highly recommended due to its excellent JavaScript support, extensions, and integrated terminal.
    • Sublime Text
    • Atom
  3. Node.js (Optional, but Recommended): Node.js is a JavaScript runtime built on Chrome’s V8 JavaScript engine. It allows you to run JavaScript code outside of a web browser (e.g., on your computer’s command line or on a server). Many modern JavaScript development tools and frameworks rely on Node.js.

Step-by-step instructions for setting up VS Code and Node.js:

Step 1: Install Visual Studio Code (VS Code)

  1. Go to the official VS Code website: https://code.visualstudio.com/
  2. Download the installer for your operating system (Windows, macOS, Linux).
  3. Run the installer and follow the instructions. It’s generally safe to accept the default options.

Step 2: Install Node.js

  1. Go to the official Node.js website: https://nodejs.org/
  2. You’ll see two download options: “LTS” (Long Term Support) and “Current”. For beginners, the LTS version is recommended as it’s more stable.
  3. Download the appropriate installer for your operating system.
  4. Run the installer. Again, it’s usually fine to stick with the default installation options. The installer will also typically install npm (Node Package Manager), which is crucial for managing JavaScript libraries.

Step 3: Verify Installation

  1. Open your computer’s terminal or command prompt (on Windows, you can search for “cmd” or “Command Prompt”; on macOS, search for “Terminal”).
  2. Type the following commands and press Enter after each:
    node -v
    npm -v
    
  3. If Node.js and npm are installed correctly, you will see their version numbers printed in the terminal (e.g., v20.19.4 for Node.js and 11.5.1 for npm).

Step 4: Create Your First JavaScript File

  1. Open VS Code.
  2. Go to File > Open Folder... (or File > Add Folder to Workspace...) and create a new empty folder on your desktop called my-javascript-project. Open this folder in VS Code.
  3. Inside VS Code, in the Explorer panel on the left, click the “New File” icon (looks like a page with a plus sign) and name the file hello.js.
  4. Type the following code into hello.js:
    console.log("Hello, JavaScript!");
    
  5. Save the file (File > Save or Ctrl+S / Cmd+S).

Step 5: Run Your First JavaScript Code

  1. In VS Code, open the integrated terminal by going to Terminal > New Terminal or by pressing Ctrl+ ` (backtick).
  2. In the terminal, make sure you are in your my-javascript-project directory. If not, use the cd command (e.g., cd Desktop/my-javascript-project).
  3. Type the following command and press Enter:
    node hello.js
    
  4. You should see Hello, JavaScript! printed in the terminal.

Congratulations! You’ve successfully set up your JavaScript development environment and run your first JavaScript program.


2. Core Concepts and Fundamentals

This section will introduce you to the fundamental building blocks of JavaScript. Understanding these concepts is crucial for writing any JavaScript code.

Variables

Variables are containers for storing data values. Think of them as named boxes where you can put different types of information.

Keywords for declaring variables:

  • var: The oldest way to declare variables. It has function scope and can be redeclared and reassigned. Due to its quirks, it’s generally less recommended for new code.
  • let: Introduced in ECMAScript 2015 (ES6). It has block scope, can be reassigned, but cannot be redeclared within the same scope. This is the preferred way to declare variables whose values might change.
  • const: Also introduced in ES6. It has block scope and cannot be reassigned or redeclared after its initial assignment. This is the preferred way to declare variables whose values should remain constant. You must initialize a const variable when you declare it.

Detailed Explanation:

  • var: Variables declared with var are “hoisted” to the top of their scope and initialized with undefined. They can be declared multiple times in the same scope without an error (though this can lead to confusion). Their scope is the entire function they are declared within, or global if declared outside any function.

  • let: let variables are also hoisted but are not initialized, leading to a “temporal dead zone” where they cannot be accessed before their declaration. They are block-scoped, meaning they are only accessible within the block ({}) where they are defined. This prevents common errors associated with var’s function-level hoisting.

  • const: const variables are also block-scoped and have a temporal dead zone. The key difference is that once a const variable is assigned a value, it cannot be reassigned. This does not mean the value itself is immutable for complex data types (objects and arrays); it means the binding to the variable name cannot be changed.

Code Examples:

// Using var (less recommended for modern JS)
var greeting = "Hello";
console.log(greeting); // Output: Hello

var greeting = "Hi there"; // Redeclared (no error, but confusing)
console.log(greeting); // Output: Hi there

// Using let (recommended for mutable variables)
let userName = "Alice";
console.log(userName); // Output: Alice

userName = "Bob"; // Reassigned (allowed)
console.log(userName); // Output: Bob

// let userName = "Charlie"; // Error: Cannot redeclare block-scoped variable 'userName'.

// Using const (recommended for constant variables)
const PI = 3.14159;
console.log(PI); // Output: 3.14159

// PI = 3.14; // Error: Assignment to constant variable.

const user = {
  name: "Eve",
  age: 30
};
console.log(user); // Output: { name: 'Eve', age: 30 }

user.age = 31; // Allowed: Changing a property of the object
console.log(user); // Output: { name: 'Eve', age: 31 }

// user = { name: "Frank", age: 25 }; // Error: Assignment to constant variable. (Trying to reassign the entire object)

Exercises/Mini-Challenges:

  1. Declare a variable named favColor using let and assign it your favorite color. Print its value to the console. Then, change its value to a different color and print it again.
  2. Declare a constant named appName using const and assign it a name for a fictional app. Try to reassign appName to a different value and observe the error.
  3. Declare a constant array named numbers with three numbers. Try to add a fourth number to the array using push() and observe if it’s allowed. (Hint: const only prevents reassignment of the variable, not modification of its contents for arrays/objects).

Data Types

JavaScript has several built-in data types that classify different kinds of values. Understanding these types is fundamental to working with data.

Primitive Data Types:

  • number: Represents both integer and floating-point numbers.
  • string: Represents textual data.
  • boolean: Represents a logical entity and can have two values: true or false.
  • undefined: Represents a variable that has been declared but not yet assigned a value.
  • null: Represents the intentional absence of any object value. It’s a primitive value.
  • symbol (ES6): Represents a unique identifier.
  • bigint (ES2020): Represents whole numbers larger than 2^53 - 1.

Non-Primitive Data Type (Object):

  • object: A complex data type that allows you to store collections of data and more complex entities. Arrays and functions are special kinds of objects.

Detailed Explanation:

  • number: JavaScript numbers are 64-bit floating-point values. This means there’s no separate integer type; all numbers are internally floats.

  • string: Strings are immutable sequences of characters. They can be enclosed in single quotes (''), double quotes (""), or backticks (``) for template literals.

  • boolean: Used for conditional logic. Operations that result in true or false.

  • undefined: When a variable is declared but not assigned, it defaults to undefined. A function that doesn’t return anything explicitly returns undefined.

  • null: Explicitly set by a developer to indicate “no value.” It’s important to distinguish from undefined.

  • symbol: Used to create unique identifiers, often for object properties to avoid naming collisions.

  • bigint: For very large integer values that exceed the safe integer limit of standard number types.

  • object: The fundamental non-primitive type. Objects are collections of key-value pairs (properties).

Code Examples:

// Number
let age = 25;
let price = 19.99;
console.log(typeof age);   // Output: number
console.log(typeof price); // Output: number

// String
let name = "John Doe";
let message = 'Hello World!';
let templateLiteral = `My name is ${name}.`;
console.log(typeof name);    // Output: string
console.log(templateLiteral); // Output: My name is John Doe.

// Boolean
let isLoggedIn = true;
let hasPermission = false;
console.log(typeof isLoggedIn); // Output: boolean

// Undefined
let quantity;
console.log(quantity);      // Output: undefined
console.log(typeof quantity); // Output: undefined

// Null
let selectedItem = null;
console.log(selectedItem);      // Output: null
console.log(typeof selectedItem); // Output: object (This is a historical bug in JavaScript, null is a primitive type)

// Symbol (ES6)
const id = Symbol('id');
const anotherId = Symbol('id');
console.log(id === anotherId); // Output: false (Symbols are unique)

// BigInt (ES2020)
const largeNumber = 1234567890123456789012345678901234567890n;
console.log(typeof largeNumber); // Output: bigint

// Object
let person = {
  firstName: "Jane",
  lastName: "Smith",
  occupation: "Developer"
};
console.log(typeof person); // Output: object

// Array (special type of object)
let colors = ["red", "green", "blue"];
console.log(typeof colors); // Output: object

Exercises/Mini-Challenges:

  1. Declare variables for the following, choosing the appropriate keyword (let or const) and data type:
    • Your first name
    • Your age
    • Whether you are a student (true/false)
    • The total cost of an item (with decimals)
    • A variable representing a user’s chosen avatar (initially no avatar, so null)
  2. Use console.log() and typeof to display the value and data type of each variable you declared.
  3. Explain in your own words the difference between undefined and null.

Operators

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

Types of Operators:

  • Assignment Operators: Assign values to variables. (=, +=, -=, *=, /=, etc.)
  • Arithmetic Operators: Perform mathematical calculations. (+, -, *, /, % (modulus), ** (exponentiation), ++ (increment), -- (decrement))
  • Comparison Operators: Compare two values and return a boolean. (==, ===, !=, !==, >, <, >=, <=)
  • Logical Operators: Combine boolean expressions. (&& (AND), || (OR), ! (NOT))
  • String Operators: Concatenate strings. (+)
  • Unary Operators: Operate on a single operand. (typeof, !, ++, --, - (negation), + (unary plus))
  • Ternary (Conditional) Operator: A shorthand for if-else statements. (condition ? expr1 : expr2)

Detailed Explanation:

  • Assignment: The most basic is =, but compound assignments (+=, etc.) are shortcuts. a += b is equivalent to a = a + b.
  • Arithmetic: Standard mathematical operations. Modulus (%) gives the remainder of a division. Exponentiation (**) raises a number to a power. Increment/decrement (++, --) add/subtract 1.
  • Comparison:
    • == (loose equality): Compares values after type coercion (tries to convert types before comparing). Often leads to unexpected results.
    • === (strict equality): Compares values and types without coercion. Always prefer === over ==.
    • != (loose inequality) vs. !== (strict inequality): Similar logic to equality. Always prefer !==.
  • Logical:
    • && (AND): Returns true if both operands are true. Short-circuits: if the first operand is false, it doesn’t evaluate the second.
    • || (OR): Returns true if at least one operand is true. Short-circuits: if the first operand is true, it doesn’t evaluate the second.
    • ! (NOT): Inverts the boolean value of the operand.
  • String: The + operator concatenates strings.
  • Unary: typeof returns the data type as a string. ! converts an operand to boolean and negates it.
  • Ternary: condition ? value_if_true : value_if_false. Useful for concise conditional assignments.

Code Examples:

// Assignment Operators
let x = 10;
x += 5; // x is now 15 (x = x + 5)
console.log("x after +=:", x); // Output: 15

// Arithmetic Operators
let a = 10;
let b = 3;
console.log("a + b:", a + b);     // Output: 13
console.log("a - b:", a - b);     // Output: 7
console.log("a * b:", a * b);     // Output: 30
console.log("a / b:", a / b);     // Output: 3.333...
console.log("a % b:", a % b);     // Output: 1 (remainder of 10 / 3)
console.log("a ** b:", a ** b);   // Output: 1000 (10 to the power of 3)

let counter = 0;
counter++; // counter is now 1
console.log("counter after ++:", counter); // Output: 1
counter--; // counter is now 0
console.log("counter after --:", counter); // Output: 0

// Comparison Operators
let num1 = 5;
let strNum = "5";
console.log("num1 == strNum:", num1 == strNum);   // Output: true (loose equality, converts string to number)
console.log("num1 === strNum:", num1 === strNum); // Output: false (strict equality, different types)
console.log("num1 != strNum:", num1 != strNum);   // Output: false
console.log("num1 !== strNum:", num1 !== strNum); // Output: true
console.log("10 > 5:", 10 > 5);                   // Output: true

// Logical Operators
let isSunny = true;
let isWarm = false;
console.log("isSunny && isWarm:", isSunny && isWarm); // Output: false (both not true)
console.log("isSunny || isWarm:", isSunny || isWarm); // Output: true (at least one is true)
console.log("!isSunny:", !isSunny);                 // Output: false

// String Operator
let firstName = "Jane";
let lastName = "Doe";
let fullName = firstName + " " + lastName;
console.log("Full name:", fullName); // Output: Jane Doe

// Ternary Operator
let ageLimit = 18;
let userAge = 20;
let canVote = (userAge >= ageLimit) ? "Yes" : "No";
console.log("Can vote:", canVote); // Output: Yes

Exercises/Mini-Challenges:

  1. Declare two variables, numA = 7 and numB = 2.
    • Calculate their sum, difference, product, and quotient. Print each result.
    • Calculate the remainder when numA is divided by numB. Print the result.
  2. Given let score = 100; and let passed = "true";
    • Use loose equality (==) to compare score with 100.
    • Use strict equality (===) to compare score with 100.
    • Use loose equality (==) to compare passed with true.
    • Use strict equality (===) to compare passed with true.
    • Explain why the results for passed are different.
  3. Declare isAdmin = true and isAuthenticated = false.
    • Write a logical expression that checks if a user is both an admin AND authenticated.
    • Write a logical expression that checks if a user is either an admin OR authenticated.
    • Write an expression that negates isAuthenticated. Print all results.

Control Flow (Conditionals and Loops)

Control flow statements determine the order in which code is executed. They allow your program to make decisions and repeat actions.

Conditional Statements (if, else if, else, switch)

Conditional statements execute different blocks of code based on whether a condition is true or false.

Detailed Explanation:

  • if statement: Executes a block of code if a specified condition is true.
  • else if statement: Used to specify a new condition to test if the first condition (or previous else if) is false. You can have multiple else if blocks.
  • else statement: Executes a block of code if all preceding if and else if conditions are false.
  • switch statement: Evaluates an expression and executes code blocks based on matching cases. It’s often a cleaner alternative to long if-else if chains when dealing with multiple possible fixed values.
    • break: Exits the switch statement once a match is found. Without break, execution “falls through” to the next case.
    • default: An optional case that executes if no other case matches the expression.

Code Examples:

// if, else if, else
let temperature = 28;

if (temperature > 30) {
  console.log("It's scorching hot!");
} else if (temperature > 20) {
  console.log("It's a pleasant day.");
} else {
  console.log("It's a bit chilly.");
}
// Output: It's a pleasant day.

// switch statement
let day = "Monday";

switch (day) {
  case "Monday":
    console.log("Start of the work week.");
    break;
  case "Friday":
    console.log("Weekend is almost here!");
    break;
  case "Saturday":
  case "Sunday": // Multiple cases can share a single block
    console.log("It's the weekend!");
    break;
  default:
    console.log("It's a regular weekday.");
}
// Output: Start of the work week.

Exercises/Mini-Challenges:

  1. Write an if-else if-else statement that checks a grade variable (e.g., 85).
    • If grade is 90 or above, print “Excellent!”.
    • If grade is between 80 and 89 (inclusive), print “Very Good!”.
    • If grade is between 70 and 79 (inclusive), print “Good.”.
    • Otherwise, print “Needs Improvement.”.
  2. Create a switch statement that evaluates a fruit variable.
    • If fruit is “apple” or “banana”, print “This is a common fruit.”.
    • If fruit is “orange”, print “This is a citrus fruit.”.
    • For any other fruit, print “I don’t know this fruit.”.

Looping Statements (for, while, do...while, for...of, for...in)

Loops execute a block of code repeatedly until a certain condition is met.

Detailed Explanation:

  • for loop: The most common loop. It’s used when you know exactly how many times you want to loop. It has three parts:

    1. initialization: Executed once before the loop starts.
    2. condition: Evaluated before each iteration. If true, the loop continues.
    3. increment/decrement: Executed after each iteration.
  • while loop: Executes a block of code as long as a specified condition is true. The condition is checked before each iteration. Be careful to avoid infinite loops by ensuring the condition eventually becomes false.

  • do...while loop: Similar to while, but guarantees that the code block is executed at least once, because the condition is checked after the first iteration.

  • for...of loop (ES6): Iterates over iterable objects (like Arrays, Strings, Maps, Sets, NodeLists, etc.), accessing the values of each element. This is generally the preferred way to iterate over arrays.

  • for...in loop: Iterates over the enumerable properties of an object. It iterates over the keys (property names). It is not recommended for iterating over arrays due to unexpected behavior (it might iterate over inherited properties and the order is not guaranteed).

Code Examples:

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

// while loop
let count = 0;
while (count < 3) {
  console.log("While loop iteration:", count);
  count++;
}
// Output:
// While loop iteration: 0
// While loop iteration: 1
// While loop iteration: 2

// do...while loop
let j = 0;
do {
  console.log("Do...while loop iteration:", j);
  j++;
} while (j < 1);
// Output: Do...while loop iteration: 0 (executes at least once)

// for...of loop (for iterating over values of iterables like arrays, strings)
const fruits = ["apple", "banana", "cherry"];
for (const fruit of fruits) {
  console.log("Fruit:", fruit);
}
// Output:
// Fruit: apple
// Fruit: banana
// Fruit: cherry

const myString = "Code";
for (const char of myString) {
  console.log("Character:", char);
}
// Output:
// Character: C
// Character: o
// Character: d
// Character: e

// for...in loop (for iterating over keys/properties of objects)
const car = {
  make: "Toyota",
  model: "Camry",
  year: 2020
};
for (const key in car) {
  console.log(`${key}: ${car[key]}`);
}
// Output:
// make: Toyota
// model: Camry
// year: 2020

Exercises/Mini-Challenges:

  1. Use a for loop to print all even numbers from 0 to 10 (inclusive).
  2. Use a while loop to repeatedly ask the user for a number using prompt() until they enter “5”. (Note: prompt() is typically used in browser environments. For Node.js, you’d use the readline module as seen in the search results example, but for a simple exercise, conceptualize prompt() as getting user input.)
  3. Given the array const colors = ["red", "green", "blue", "yellow"];, use a for...of loop to print each color.
  4. Given the object const product = { name: "Laptop", price: 1200, category: "Electronics" };, use a for...in loop to print each property name and its value.

Functions

Functions are blocks of reusable code designed to perform a particular task. They allow you to organize your code, make it more modular, and avoid repetition.

Types of Functions:

  • Function Declarations (Named Functions): The traditional way to define a function. They are hoisted, meaning you can call them before they are defined in your code.
  • Function Expressions: Functions assigned to a variable. They are not hoisted like declarations, so you must define them before you call them.
  • Arrow Functions (ES6): A more concise syntax for writing function expressions. They have a different way of handling this keyword (lexical this), which makes them very useful in certain contexts, especially for callbacks.

Detailed Explanation:

  • Declaring a Function: You define a function with the function keyword, followed by the function name, a list of parameters in parentheses, and the function body enclosed in curly braces.
  • Parameters and Arguments:
    • Parameters: The names listed in the function definition’s parentheses (e.g., (param1, param2)). They are placeholders for values.
    • Arguments: The actual values passed to the function when it is called.
  • return statement: Specifies the value a function should send back to the caller. If a function doesn’t have a return statement, it implicitly returns undefined.
  • Function Scope: Variables declared inside a function are local to that function and cannot be accessed from outside.
  • Arrow Functions and this: A key difference is how arrow functions handle the this keyword. Unlike regular functions, arrow functions do not have their own this binding. They inherit this from the surrounding (lexical) context. This makes them ideal for callback functions where this behavior can be tricky with traditional functions.

Code Examples:

// Function Declaration
function greet(name) {
  return `Hello, ${name}!`;
}
console.log(greet("Alice")); // Output: Hello, Alice!

// Function Expression
const calculateArea = function(width, height) {
  return width * height;
};
console.log(calculateArea(5, 10)); // Output: 50

// Arrow Function (concise syntax, useful for simple functions or callbacks)
const add = (a, b) => a + b;
console.log(add(7, 3)); // Output: 10

const sayGoodbye = name => { // Parentheses optional for single parameter
  console.log(`Goodbye, ${name}.`);
};
sayGoodbye("Bob"); // Output: Goodbye, Bob.

const multiply = (x, y) => { // Curly braces needed for multiple statements
  const result = x * y;
  return result;
};
console.log(multiply(4, 5)); // Output: 20

Exercises/Mini-Challenges:

  1. Create a function declaration called celsiusToFahrenheit that takes one parameter, celsius, and returns the temperature in Fahrenheit. The formula is (Celsius * 9/5) + 32. Test it with a few values.
  2. Write a function expression named isEven that takes a number as input and returns true if the number is even, and false otherwise.
  3. Convert the isEven function expression into an arrow function.
  4. Write an arrow function countCharacters that takes a string and returns the number of characters in the string. (Hint: strings have a length property).

Arrays

Arrays are ordered lists of values. They are a fundamental data structure for storing collections of data.

Detailed Explanation:

  • Creation: Arrays are created using square brackets [] and elements are separated by commas.
  • Indexing: Elements in an array are accessed using a zero-based index (the first element is at index 0, the second at index 1, and so on).
  • Length: The length property returns the number of elements in the array.
  • Mutability: Arrays are mutable, meaning you can change their elements after creation.
  • Common Array Methods: JavaScript provides many built-in methods for working with arrays. Some essential ones include:
    • push(): Adds one or more elements to the end of an array.
    • pop(): Removes the last element from an array and returns that element.
    • shift(): Removes the first element from an array and returns that element.
    • unshift(): Adds one or more elements to the beginning of an array.
    • indexOf(): Returns the first index at which a given element can be found in the array, or -1 if it is not present.
    • includes(): Determines whether an array includes a certain value among its entries, returning true or false.
    • slice(): Returns a shallow copy of a portion of an array into a new array.
    • splice(): Changes the contents of an array by removing or replacing existing elements and/or adding new elements in place. (Modifies the original array).
    • forEach(): Executes a provided function once for each array element.
    • map(): Creates a new array populated with the results of calling a provided function on every element in the calling array.
    • filter(): Creates a new array with all elements that pass the test implemented by the provided function.
    • find(): Returns the value of the first element in the provided array that satisfies the provided testing function.
    • reduce(): Executes a reducer function on each element of the array, resulting in a single output value.

Code Examples:

// Creating an array
let shoppingList = ["milk", "bread", "eggs"];
console.log(shoppingList); // Output: [ 'milk', 'bread', 'eggs' ]

// Accessing elements
console.log(shoppingList[0]); // Output: milk (first element)
console.log(shoppingList[2]); // Output: eggs (third element)

// Getting array length
console.log(shoppingList.length); // Output: 3

// Modifying elements
shoppingList[1] = "butter";
console.log(shoppingList); // Output: [ 'milk', 'butter', 'eggs' ]

// Adding/Removing elements
shoppingList.push("cheese"); // Adds to end
console.log(shoppingList); // Output: [ 'milk', 'butter', 'eggs', 'cheese' ]

shoppingList.pop(); // Removes last element ("cheese")
console.log(shoppingList); // Output: [ 'milk', 'butter', 'eggs' ]

shoppingList.unshift("juice"); // Adds to beginning
console.log(shoppingList); // Output: [ 'juice', 'milk', 'butter', 'eggs' ]

shoppingList.shift(); // Removes first element ("juice")
console.log(shoppingList); // Output: [ 'milk', 'butter', 'eggs' ]

// Iterating with forEach
shoppingList.forEach(item => {
  console.log(`Don't forget to buy ${item}`);
});
// Output:
// Don't forget to buy milk
// Don't forget to buy butter
// Don't forget to buy eggs

// map - creating a new array from an existing one
const numbers = [1, 2, 3];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers); // Output: [2, 4, 6]

// filter - creating a new array with filtered elements
const ages = [12, 18, 25, 6];
const adults = ages.filter(age => age >= 18);
console.log(adults); // Output: [18, 25]

Exercises/Mini-Challenges:

  1. Create an array named hobbies with at least three of your favorite hobbies.
    • Add a new hobby to the end of the array.
    • Remove the first hobby from the array.
    • Print the array after each modification.
  2. Given the array const temperatures = [20, 22, 18, 25, 21];
    • Use forEach() to print each temperature with a message like “The temperature is X degrees Celsius.”
    • Use map() to create a new array fahrenheitTemps where each temperature is converted to Fahrenheit. (Reuse your celsiusToFahrenheit function if you made it!). Print fahrenheitTemps.
    • Use filter() to create a new array warmTemps containing only temperatures greater than or equal to 22. Print warmTemps.

Objects

Objects are a fundamental JavaScript data type used to store collections of data and more complex entities. Unlike arrays, which store ordered lists, objects store data as unordered key-value pairs.

Detailed Explanation:

  • Creation: Objects are typically created using curly braces {} with properties defined as key: value pairs. Keys (or property names) are strings (or Symbols, though strings are more common), and values can be any JavaScript data type, including other objects or functions.
  • Accessing Properties:
    • Dot Notation (object.property): Preferred when the property name is a valid identifier (doesn’t contain spaces, starts with a letter, etc.) and you know the property name beforehand.
    • Bracket Notation (object['property']): Used when the property name is dynamically determined (e.g., stored in a variable), contains special characters (like spaces or hyphens), or starts with a number.
  • Adding/Modifying Properties: You can add new properties or modify existing ones simply by assigning a value using either dot or bracket notation.
  • Deleting Properties: The delete operator removes a property from an object.
  • Methods: Functions stored as object properties are called methods. They define behavior for the object.
  • this keyword in Objects: Inside an object method, this refers to the object itself, allowing the method to access other properties of the same object.

Code Examples:

// Creating an object
const book = {
  title: "The Great Gatsby",
  author: "F. Scott Fitzgerald",
  year: 1925,
  isAvailable: true,
  genres: ["Classic", "Fiction"]
};
console.log(book);
// Output: { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925, isAvailable: true, genres: [ 'Classic', 'Fiction' ] }

// Accessing properties
console.log("Book title (dot notation):", book.title); // Output: The Great Gatsby
console.log("Book author (bracket notation):", book['author']); // Output: F. Scott Fitzgerald

const propertyName = "year";
console.log("Book year (dynamic bracket notation):", book[propertyName]); // Output: 1925

// Adding new properties
book.publisher = "Scribner";
console.log("Book with new publisher:", book);

// Modifying existing properties
book.isAvailable = false;
console.log("Book availability updated:", book.isAvailable); // Output: false

// Deleting properties
delete book.genres;
console.log("Book after deleting genres:", book);

// Object with a method
const dog = {
  name: "Buddy",
  breed: "Golden Retriever",
  bark: function() {
    console.log(`${this.name} says Woof!`); // `this` refers to the dog object
  }
};
dog.bark(); // Output: Buddy says Woof!

Exercises/Mini-Challenges:

  1. Create an object named myCar with the following properties: make, model, year, and color. Assign appropriate values.
    • Print the model of your car using dot notation.
    • Change the color of your car.
    • Add a new property isElectric and set it to true or false.
    • Print the entire myCar object after all modifications.
  2. Add a method named getCarInfo to the myCar object. This method should return a string like: “My [color] [year] [make] [model]”. Call this method and print its returned value.
  3. Given the object const userProfile = { username: "coder123", email: "coder@example.com" };. Try to access a non-existent property (e.g., userProfile.password) and observe what value is returned.

3. Intermediate Topics

Now that you have a solid grasp of JavaScript fundamentals, let’s explore some intermediate concepts that will significantly enhance your coding abilities.

Asynchronous JavaScript (Callbacks, Promises, Async/Await)

JavaScript is single-threaded, meaning it executes one operation at a time. However, many operations (like fetching data from a server, reading a file, or timing events) can take time. If JavaScript waited for these operations to complete, the entire application would freeze. Asynchronous JavaScript allows these long-running operations to happen in the “background” without blocking the main thread, and then respond once they are finished.

Callbacks

The oldest way to handle asynchronous operations. A callback function is simply a function that is passed as an argument to another function and is executed after the first function has completed its operation.

Detailed Explanation:

Callbacks are commonly used in Node.js for I/O operations and in older browser APIs (like setTimeout). While simple for basic cases, they can lead to “callback hell” or “pyramid of doom” when dealing with multiple nested asynchronous operations, making code hard to read and maintain.

Code Examples:

// Simulate fetching data after a delay
function fetchData(callback) {
  setTimeout(() => {
    const data = "Some data from the server";
    console.log("Data fetched!");
    callback(data); // Call the callback function with the fetched data
  }, 2000); // Simulate a 2-second delay
}

function processData(data) {
  console.log(`Processing: "${data}"`);
}

console.log("Starting data fetch...");
fetchData(processData); // Pass processData as the callback
console.log("Fetch initiated, continuing other tasks...");

// Output (order might vary slightly depending on how the console logs buffer):
// Starting data fetch...
// Fetch initiated, continuing other tasks...
// (2-second delay)
// Data fetched!
// Processing: "Some data from the server"

Exercises/Mini-Challenges:

  1. Write a function delayedGreeting that takes a name and a delay (in milliseconds) as arguments. It should use setTimeout to call a callback function after the specified delay. The callback function should print “Hello, [name]!”
  2. Modify delayedGreeting to include an optional error parameter in the callback. If the name is empty, call the callback with an error message, otherwise call it with a success message. Test both scenarios.

Promises

Promises are a more structured and robust way to handle asynchronous operations, introduced in ES6. They represent the eventual completion (or failure) of an asynchronous operation and its resulting value.

Detailed Explanation:

A Promise can be in one of three states:

  • pending: Initial state, neither fulfilled nor rejected.
  • fulfilled: Meaning that the operation completed successfully.
  • rejected: Meaning that the operation failed.

Promises are consumed using .then() for successful outcomes and .catch() for errors. They allow for chaining multiple asynchronous operations, making the code much flatter and easier to read than nested callbacks.

Code Examples:

// Simulate an asynchronous operation that might succeed or fail
function checkStock(itemId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (itemId === "apple") {
        resolve({
          item: "apple",
          quantity: 10
        }); // Operation successful
      } else {
        reject(new Error("Item not found in stock!")); // Operation failed
      }
    }, 1500);
  });
}

console.log("Checking stock for apple...");
checkStock("apple")
  .then(data => {
    console.log(`Stock for ${data.item}: ${data.quantity}`);
    return data.quantity * 2; // Pass data to the next .then()
  })
  .then(doubleQuantity => {
    console.log(`Double quantity: ${doubleQuantity}`);
  })
  .catch(error => {
    console.error("Error:", error.message);
  });

console.log("\nChecking stock for orange...");
checkStock("orange")
  .then(data => {
    console.log(`Stock for ${data.item}: ${data.quantity}`);
  })
  .catch(error => {
    console.error("Error:", error.message);
  });

// Output (with delays):
// Checking stock for apple...
// Checking stock for orange...
// (1.5-second delay)
// Stock for apple: 10
// Double quantity: 20
// Error: Item not found in stock!

Exercises/Mini-Challenges:

  1. Write a function simulateLogin that returns a Promise. The promise should resolve after 1 second with a message “Login successful!” if a username parameter is “admin” and password is “password123”. Otherwise, it should reject with “Invalid credentials.”
  2. Call simulateLogin with correct and incorrect credentials, using .then() and .catch() to handle success and error messages.
  3. Explore Promise.all(). Create three small functions that return promises, each resolving with a number after a different delay. Use Promise.all() to wait for all three to resolve and then log their sum.

Async/Await

async and await are modern JavaScript syntax (ES2017) built on top of Promises, providing a more synchronous-looking way to write asynchronous code, making it even easier to read and debug.

Detailed Explanation:

  • async function: A function declared with the async keyword automatically returns a Promise. Inside an async function, you can use the await keyword.
  • await keyword: Can only be used inside an async function. It pauses the execution of the async function until the Promise it’s waiting for settles (resolves or rejects). If the Promise resolves, await returns its resolved value. If it rejects, await throws an error, which can be caught using try...catch blocks.

async/await makes asynchronous code look and behave a lot more like synchronous code, making complex asynchronous flows much more manageable.

Code Examples:

// Re-using the checkStock function from Promises example
function checkStock(itemId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (itemId === "apple") {
        resolve({
          item: "apple",
          quantity: 10
        });
      } else {
        reject(new Error("Item not found in stock!"));
      }
    }, 1000);
  });
}

async function orderItem(item) {
  try {
    console.log(`Attempting to order ${item}...`);
    const stockInfo = await checkStock(item); // Pause here until promise resolves
    console.log(`${stockInfo.item} found in stock! Quantity: ${stockInfo.quantity}`);

    const finalOrder = `Order placed for ${stockInfo.quantity} units of ${stockInfo.item}.`;
    return finalOrder;
  } catch (error) {
    console.error(`Could not order ${item}: ${error.message}`);
    return `Order failed for ${item}.`;
  }
}

async function runOrders() {
  const result1 = await orderItem("apple");
  console.log(result1);

  console.log("\n--- Next Order ---"); // This will execute after the first order completes

  const result2 = await orderItem("orange");
  console.log(result2);
}

runOrders();

// Output (with delays):
// Attempting to order apple...
// --- Next Order ---
// Attempting to order orange...
// (1-second delay for apple)
// apple found in stock! Quantity: 10
// Order placed for 10 units of apple.
// (1-second delay for orange, executed concurrently to some extent but resolved later)
// Could not order orange: Item not found in stock!
// Order failed for orange.

Exercises/Mini-Challenges:

  1. Convert your simulateLogin function from the Promises exercise to use async/await. Call it with both successful and unsuccessful attempts using try...catch.
  2. Write an async function fetchAndDisplayUser that simulates fetching user data from an API. Inside the function:
    • Simulate a network request using setTimeout wrapped in a Promise. This Promise should resolve with a user object { id: 1, name: "Alice", email: "alice@example.com" } after 1.5 seconds.
    • await the result of this “API call”.
    • Print the user’s name and email.
    • Implement error handling using try...catch for cases where the “fetch” might fail (e.g., if you simulate a network error by always rejecting the Promise).

Error Handling (try...catch, finally, throw)

Errors are an inevitable part of programming. Effective error handling ensures your applications can gracefully recover from unexpected issues, providing a better user experience and making debugging easier.

Detailed Explanation:

  • try block: Contains the code that might potentially throw an error.
  • catch block: Contains the code that executes if an error occurs within the try block. It receives an error object as an argument, which contains information about the error.
  • finally block: Contains code that will always execute, regardless of whether an error occurred or was caught. It’s often used for cleanup operations (e.g., closing file connections, releasing resources).
  • throw statement: Allows you to create and throw a custom error. When an error is thrown, the normal flow of execution stops, and JavaScript looks for a catch block to handle it. If no catch block is found, the program will terminate.
  • Error object: The built-in Error object provides useful properties like name (e.g., “ReferenceError”, “TypeError”, “SyntaxError”) and message (a human-readable description of the error).

Code Examples:

// Basic try...catch
function divide(a, b) {
  try {
    if (b === 0) {
      throw new Error("Cannot divide by zero!"); // Throw a custom error
    }
    return a / b;
  } catch (error) {
    console.error("Caught an error:", error.message);
    return null; // Return a sensible default or indication of failure
  }
}

console.log(divide(10, 2)); // Output: 5
console.log(divide(10, 0)); // Output: Caught an error: Cannot divide by zero!, then null

// try...catch...finally
function processFile(fileName) {
  try {
    console.log(`Attempting to open ${fileName}...`);
    // Simulate file reading that might fail
    if (fileName === "nonexistent.txt") {
      throw new Error("File not found!");
    }
    console.log(`Successfully read ${fileName}`);
    return "File content here";
  } catch (error) {
    console.error(`Error processing file: ${error.message}`);
    return null;
  } finally {
    // This block always runs, useful for cleanup
    console.log("Finished attempting to process file (closing resources if any).");
  }
}

processFile("mydata.txt");
processFile("nonexistent.txt");

// Output for mydata.txt:
// Attempting to open mydata.txt...
// Successfully read mydata.txt
// Finished attempting to process file (closing resources if any).

// Output for nonexistent.txt:
// Attempting to open nonexistent.txt...
// Error processing file: File not found!
// Finished attempting to process file (closing resources if any).

Exercises/Mini-Challenges:

  1. Write a function validateAge that takes an age as input.
    • If age is less than 0 or greater than 120, throw new Error("Invalid age provided.");
    • Otherwise, return true.
    • Call this function within a try...catch block. Test with valid and invalid ages and print appropriate messages.
  2. Enhance your celsiusToFahrenheit function.
    • Add a try...catch block inside it.
    • If the input celsius is not a number (use typeof), throw new TypeError("Input must be a number.");
    • In the catch block, log the error message and return NaN (Not-a-Number).
    • Test with a number and a string input.

Event Handling

Event handling is crucial for making web pages interactive. It allows your JavaScript code to respond to user actions (like clicks, key presses, form submissions) or browser events (like page loading).

Detailed Explanation:

In web browsers, the Document Object Model (DOM) represents the structure of a web page. Elements in the DOM can emit events. You can “listen” for these events and execute a function (an event handler) when they occur.

  • addEventListener(): The preferred way to register event handlers.
    • It allows multiple handlers for the same event on the same element.
    • It separates HTML, CSS, and JavaScript.
    • target.addEventListener(type, listener[, options]);
      • type: A string representing the event type (e.g., 'click', 'mouseover', 'submit').
      • listener: The function to be called when the event occurs.
      • options (optional): An object that can configure event listener behavior (e.g., once: true to run the handler only once, capture: true for event capturing phase).
  • Common Events:
    • Mouse Events: click, dblclick, mouseover, mouseout, mousedown, mouseup, mousemove
    • Keyboard Events: keydown, keyup, keypress
    • Form Events: submit, input, change, focus, blur
    • Document/Window Events: load, DOMContentLoaded, resize, scroll
  • Event Object: When an event handler is called, it automatically receives an Event object as its first argument. This object contains information about the event (e.g., event.target (the element that triggered the event), event.type, event.preventDefault(), event.stopPropagation()).
  • event.preventDefault(): Stops the browser’s default action for an event (e.g., preventing a form from submitting, or a link from navigating).
  • event.stopPropagation(): Stops the event from “bubbling up” to parent elements in the DOM.

Code Examples (Requires HTML to run in a browser):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Event Handling Example</title>
  <style>
    #myButton {
      padding: 10px 20px;
      font-size: 16px;
      cursor: pointer;
    }
    #myDiv {
      width: 200px;
      height: 100px;
      background-color: lightblue;
      border: 1px solid blue;
      margin-top: 20px;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    #myForm {
      margin-top: 20px;
      border: 1px solid grey;
      padding: 10px;
    }
  </style>
</head>
<body>

  <h1>Event Handling</h1>

  <button id="myButton">Click Me!</button>

  <div id="myDiv">
    Hover over me!
  </div>

  <form id="myForm">
    <label for="nameInput">Name:</label>
    <input type="text" id="nameInput" value="Enter your name">
    <button type="submit">Submit</button>
  </form>

  <script>
    // 1. Get references to HTML elements
    const myButton = document.getElementById("myButton");
    const myDiv = document.getElementById("myDiv");
    const myForm = document.getElementById("myForm");
    const nameInput = document.getElementById("nameInput");

    // 2. Add event listeners

    // Click event on a button
    myButton.addEventListener('click', function(event) {
      console.log("Button clicked!");
      console.log("Event type:", event.type);
      console.log("Target element:", event.target.id);
      event.target.textContent = "Clicked!"; // Change button text
    });

    // Mouse events on a div
    myDiv.addEventListener('mouseover', function() {
      myDiv.style.backgroundColor = "lightcoral";
      console.log("Mouse over div!");
    });

    myDiv.addEventListener('mouseout', function() {
      myDiv.style.backgroundColor = "lightblue";
      console.log("Mouse out of div!");
    });

    // Form submission event (with preventDefault)
    myForm.addEventListener('submit', function(event) {
      event.preventDefault(); // Prevents the default form submission (page reload)
      console.log("Form submitted!");
      const nameValue = nameInput.value;
      console.log(`Name entered: ${nameValue}`);
      alert(`Hello, ${nameValue}! (Form submission prevented)`);
    });

    // Input event on a text field
    nameInput.addEventListener('input', function(event) {
      console.log("Input value changed:", event.target.value);
    });

    // Event listener with options (e.g., 'once')
    myButton.addEventListener('click', function() {
      console.log("This message will only show once!");
    }, { once: true });
  </script>
</body>
</html>

To run this example:

  1. Save the code above as an HTML file (e.g., events.html).
  2. Open the HTML file in your web browser.
  3. Open the browser’s developer console (usually F12 or Ctrl+Shift+I on Windows/Linux, Cmd+Option+I on macOS).
  4. Interact with the elements (click the button, hover the div, type in the input, submit the form) and observe the console output.

Exercises/Mini-Challenges:

  1. In the events.html file, add a new <h1> element with the ID pageTitle.
    • Add a dblclick event listener to pageTitle. When double-clicked, it should change its text content to “JavaScript Rocks!”.
  2. Add a new button with the ID clearConsoleButton.
    • Add a click event listener to this button that clears the browser console. (Hint: console.clear() in the browser).
  3. Add an image element (<img>) to your HTML.
    • When the mouse hovers over the image, change its src attribute to a different image URL.
    • When the mouse leaves the image, change it back to the original src. (You’ll need two image URLs for this, even if they are placeholders).

DOM Manipulation

The Document Object Model (DOM) is a programming interface for web documents. It represents the page structure as a tree of objects, which JavaScript can access and manipulate. DOM manipulation means using JavaScript to change the content, structure, or style of a web page after it has been loaded.

Detailed Explanation:

  • Selecting Elements: Before you can manipulate an element, you need to select it.
    • document.getElementById('idName'): Selects a single element by its unique id.
    • document.querySelector('selector'): Selects the first element that matches a specified CSS selector (e.g., '#myId', '.myClass', 'div', 'input[type="text"]').
    • document.querySelectorAll('selector'): Selects all elements that match a specified CSS selector, returning a NodeList (which can be iterated over like an array).
    • document.getElementsByClassName('className'): Selects all elements with a given class name, returning an HTMLCollection.
    • document.getElementsByTagName('tagName'): Selects all elements with a given tag name (e.g., p, div), returning an HTMLCollection.
  • Modifying Content:
    • element.textContent: Gets or sets the text content of an element (no HTML parsing).
    • element.innerHTML: Gets or sets the HTML content of an element (parses HTML, can be a security risk if used with untrusted input).
  • Modifying Attributes:
    • element.getAttribute('attributeName'): Gets the value of an attribute.
    • element.setAttribute('attributeName', 'value'): Sets the value of an attribute.
    • element.removeAttribute('attributeName'): Removes an attribute.
    • Direct property access (e.g., element.src, element.href, element.value, element.id, element.className).
  • Modifying Styles:
    • element.style.propertyName: Directly sets inline CSS styles. (e.g., element.style.color = 'blue';, element.style.fontSize = '20px';).
  • Modifying Classes:
    • element.classList.add('className'): Adds one or more classes.
    • element.classList.remove('className'): Removes one or more classes.
    • element.classList.toggle('className'): Toggles a class (adds if not present, removes if present).
    • element.classList.contains('className'): Checks if an element has a specific class.
  • Creating New Elements:
    • document.createElement('tagName'): Creates a new HTML element (e.g., document.createElement('div')).
    • document.createTextNode('text'): Creates a text node.
  • Appending/Removing Elements:
    • parentNode.appendChild(childNode): Adds a node as the last child of a parent.
    • parentNode.removeChild(childNode): Removes a child node from its parent.
    • element.remove(): A simpler way to remove an element (supported by modern browsers).

Code Examples (Requires HTML to run in a browser):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>DOM Manipulation Example</title>
  <style>
    .highlight {
      background-color: yellow;
      border: 2px solid orange;
      padding: 5px;
    }
    .red-text {
      color: red;
    }
  </style>
</head>
<body>

  <h1 id="mainTitle">Welcome to DOM Mastery</h1>
  <p class="description">This is a paragraph about DOM manipulation.</p>
  <ul id="myList">
    <li class="item">Item 1</li>
    <li class="item">Item 2</li>
  </ul>
  <button id="changeTextBtn">Change Title</button>
  <button id="addItemBtn">Add List Item</button>
  <button id="removeFirstItemBtn">Remove First Item</button>
  <div id="dynamicContent"></div>

  <script>
    // 1. Select elements
    const mainTitle = document.getElementById("mainTitle");
    const descriptionParagraph = document.querySelector(".description");
    const myList = document.getElementById("myList");
    const items = document.querySelectorAll(".item");
    const changeTextBtn = document.getElementById("changeTextBtn");
    const addItemBtn = document.getElementById("addItemBtn");
    const removeFirstItemBtn = document.getElementById("removeFirstItemBtn");
    const dynamicContentDiv = document.getElementById("dynamicContent");

    // 2. Modify content and attributes
    changeTextBtn.addEventListener('click', () => {
      mainTitle.textContent = "DOM Manipulation in Action!";
      descriptionParagraph.innerHTML = "This paragraph now has <strong>bold</strong> text.";
      descriptionParagraph.classList.add('highlight'); // Add a class
      descriptionParagraph.style.color = "purple"; // Apply inline style
    });

    // 3. Iterate and modify multiple elements
    items.forEach((item, index) => {
      item.style.backgroundColor = index % 2 === 0 ? "lightgrey" : "white";
      item.setAttribute("data-index", index); // Add a custom data attribute
    });

    // 4. Create and append new elements
    addItemBtn.addEventListener('click', () => {
      const newItem = document.createElement("li"); // Create a new <li>
      newItem.textContent = `New Item ${myList.children.length + 1}`;
      newItem.classList.add('item', 'red-text'); // Add multiple classes
      myList.appendChild(newItem); // Append to the <ul>
    });

    // 5. Remove elements
    removeFirstItemBtn.addEventListener('click', () => {
      const firstItem = document.querySelector("#myList .item");
      if (firstItem) {
        firstItem.remove(); // Remove the element
        console.log("First item removed.");
      } else {
        console.log("No more items to remove!");
      }
    });

    // Add some initial dynamic content
    const introParagraph = document.createElement('p');
    introParagraph.textContent = "This content was added dynamically!";
    dynamicContentDiv.appendChild(introParagraph);
  </script>
</body>
</html>

To run this example:

  1. Save the code above as an HTML file (e.g., dom-manipulation.html).
  2. Open the HTML file in your web browser.
  3. Open the browser’s developer console.
  4. Click the buttons and observe the changes on the page and in the console.

Exercises/Mini-Challenges:

  1. In dom-manipulation.html:
    • Add an <a> (anchor) tag with the ID myLink and href set to https://www.google.com.
    • Using JavaScript, select myLink and change its href to https://developer.mozilla.org/ and its textContent to “MDN Web Docs”.
    • Add a class highlight to myLink (make sure you have .highlight CSS in your style block).
  2. Add a new div with the ID colorBox to your HTML.
    • Using JavaScript, set its width to 100px, height to 100px, and backgroundColor to blue using element.style.
    • Add a click event listener to colorBox. When clicked, it should toggle the class red-text (which makes text red in the provided CSS, but here it’s on a div, so perhaps just for demonstration, you might need to adjust the CSS to change the background or border instead, or just understand toggleClass works). For a better visual, make .red-text also change the background to red if colorBox doesn’t have text. Or just add some text to colorBox to see the text color change.
  3. Create a new button with the ID createImageBtn.
    • When clicked, this button should create a new <img> element. Set its src to a placeholder image URL (e.g., https://via.placeholder.com/150) and alt to “Placeholder Image”.
    • Append this new image to the dynamicContentDiv.

Scope (Global, Function, Block) and Hoisting

Understanding how variables and functions are accessible within your code is crucial for avoiding unexpected behavior and writing clean, maintainable JavaScript.

Scope

Scope defines the accessibility of variables, functions, and objects in some particular part of your code.

Detailed Explanation:

  • Global Scope:
    • Variables declared outside of any function or block live in the global scope.
    • They are accessible from anywhere in your code (inside functions, blocks, etc.).
    • Minimizing global variables is a best practice to avoid naming conflicts and make code more modular.
  • Function Scope:
    • Variables declared with var inside a function are function-scoped. They are only accessible within that function.
    • Variables declared inside a function with let or const also have function scope if there are no inner blocks.
  • Block Scope (ES6: let, const):
    • Variables declared with let or const inside any block (e.g., if statements, for loops, or simply {}) are block-scoped. They are only accessible within that specific block. This is a significant improvement over var.

Code Examples:

// Global Scope
let globalVar = "I am a global variable.";
const GLOBAL_CONSTANT = "I am a global constant.";

function demonstrateScope() {
  // Function Scope (for var)
  var functionVar = "I am function-scoped (var).";
  let functionLet = "I am function-scoped (let).";
  const functionConst = "I am function-scoped (const).";

  console.log(globalVar); // Accessible
  console.log(GLOBAL_CONSTANT); // Accessible
  console.log(functionVar); // Accessible
  console.log(functionLet); // Accessible
  console.log(functionConst); // Accessible

  if (true) {
    // Block Scope
    let blockLet = "I am block-scoped (let).";
    const blockConst = "I am block-scoped (const).";
    var blockVar = "I am technically function-scoped (var in a block)."; // Hoisted to function scope

    console.log(blockLet); // Accessible
    console.log(blockConst); // Accessible
    console.log(blockVar); // Accessible
  }

  // console.log(blockLet); // Error: blockLet is not defined (outside its block)
  // console.log(blockConst); // Error: blockConst is not defined (outside its block)
  console.log(blockVar); // Accessible! (due to var's function-scoping despite being in a block)
}

demonstrateScope();

// console.log(functionVar); // Error: functionVar is not defined (outside its function)

Hoisting

Hoisting is a JavaScript mechanism where variable and function declarations are moved to the top of their containing scope during the compilation phase, before code execution.

Detailed Explanation:

  • var declarations are hoisted and initialized to undefined: This means you can reference a var variable before it’s declared, but its value will be undefined at that point.
  • let and const declarations are hoisted but not initialized: They are placed in a “Temporal Dead Zone” (TDZ) from the beginning of the block until their declaration. Attempting to access them before their declaration will result in a ReferenceError. This prevents the common undefined issue with var.
  • Function declarations are fully hoisted: You can call a function declared with function keyword before its definition in the code.
  • Function expressions (including arrow functions) are not fully hoisted: Only the variable name they are assigned to is hoisted (like var or let). The function definition itself is not. So, you can’t call a function expression before its definition.

Code Examples:

// Hoisting with var
console.log(a); // Output: undefined
var a = 5;
console.log(a); // Output: 5

// Hoisting with let and const (Temporal Dead Zone)
// console.log(b); // Error: ReferenceError: Cannot access 'b' before initialization
let b = 10;
console.log(b);

// console.log(C); // Error: ReferenceError: Cannot access 'C' before initialization
const C = 20;
console.log(C);

// Function Declaration Hoisting
sayHello(); // Output: Hello from function declaration!
function sayHello() {
  console.log("Hello from function declaration!");
}

// Function Expression (not hoisted like declarations)
// sayGoodbye(); // Error: TypeError: sayGoodbye is not a function (if var) or ReferenceError (if let/const)
const sayGoodbye = function() {
  console.log("Goodbye from function expression!");
};
sayGoodbye(); // Output: Goodbye from function expression!

// Arrow Function (similar to function expression in hoisting behavior)
// greetArrow(); // Error: TypeError: greetArrow is not a function (if var) or ReferenceError (if let/const)
const greetArrow = () => {
  console.log("Greetings from arrow function!");
};
greetArrow(); // Output: Greetings from arrow function!

Exercises/Mini-Challenges:

  1. Predict the output:
    function testHoisting() {
      console.log(x);
      var x = 10;
      console.log(x);
    }
    testHoisting();
    
    Then, run the code and verify your prediction. Explain why it behaved that way.
  2. Predict the output:
    function testTemporalDeadZone() {
      console.log(y);
      let y = 20;
      console.log(y);
    }
    testTemporalDeadZone();
    
    Then, run the code and verify your prediction. Explain why it behaved that way.
  3. Write two simple functions: one as a function declaration and one as a function expression. Try calling both before their definitions and observe the differences in behavior.

ES6+ Features (Spread/Rest, Destructuring, Template Literals, Classes)

ECMAScript 2015 (ES6) and subsequent versions introduced many powerful features that make JavaScript more pleasant to write, more powerful, and more expressive.

Spread and Rest Operators (...)

The spread and rest operators both use the ... syntax, but they serve different purposes depending on where they are used.

Detailed Explanation:

  • Spread Operator (...):

    • In Arrays/Objects (Array/Object Literals): Expands an iterable (like an array) or an object into individual elements/properties. This is useful for creating copies, concatenating arrays, or merging objects without modifying the originals.
    • In Function Calls (Function Arguments): Spreads an array’s elements as individual arguments to a function.
  • Rest Parameters (...):

    • In Function Definitions (Function Parameters): Gathers an indefinite number of arguments into an array. It must be the last parameter in a function definition.

Code Examples:

// Spread Operator with Arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combinedArr = [...arr1, ...arr2]; // Combines arrays
console.log("Combined Array:", combinedArr); // Output: [1, 2, 3, 4, 5, 6]

const copiedArr = [...arr1]; // Creates a shallow copy
console.log("Copied Array:", copiedArr);
console.log(copiedArr === arr1); // Output: false (different memory references)

// Spread Operator with Objects (shallow copy/merge)
const obj1 = {
  a: 1,
  b: 2
};
const obj2 = {
  c: 3,
  d: 4
};
const mergedObj = { ...obj1,
  ...obj2
}; // Merges objects
console.log("Merged Object:", mergedObj); // Output: { a: 1, b: 2, c: 3, d: 4 }

const updatedObj = { ...obj1,
  b: 20
}; // Overwrites existing property
console.log("Updated Object:", updatedObj); // Output: { a: 1, b: 20 }

// Spread Operator in function calls
function sum(x, y, z) {
  return x + y + z;
}
const nums = [10, 20, 30];
console.log("Sum with spread:", sum(...nums)); // Output: 60

// Rest Parameters in function definitions
function greetUsers(greeting, ...names) { // 'names' will be an array of all subsequent arguments
  names.forEach(name => console.log(`${greeting} ${name}!`));
}
greetUsers("Hello", "Alice", "Bob", "Charlie");
// Output:
// Hello Alice!
// Hello Bob!
// Hello Charlie!

function average(...numbers) { // Gathers all arguments into 'numbers' array
  if (numbers.length === 0) return 0;
  const total = numbers.reduce((acc, num) => acc + num, 0);
  return total / numbers.length;
}
console.log("Average of 1,2,3,4,5:", average(1, 2, 3, 4, 5)); // Output: 3
console.log("Average of no numbers:", average()); // Output: 0

Exercises/Mini-Challenges:

  1. Given two arrays: const arrayA = [1, 2]; and const arrayB = [3, 4];.
    • Use the spread operator to create a new array combined that contains all elements from arrayA followed by all elements from arrayB. Print combined.
    • Create a shallow copy of arrayA called copyOfA. Modify copyOfA by adding an element. Print both arrayA and copyOfA to demonstrate they are independent.
  2. Given an object const user = { id: 1, name: "Max" };.
    • Use the spread operator to create a new object adminUser that includes all properties of user and adds a new property role: "admin". Print adminUser.
    • Use the spread operator to create updatedUser from user, changing the name to “Maximilian”. Print updatedUser.
  3. Write a function logDetails(id, ...messages) that takes an id and then an indefinite number of messages. It should print the id once, then each message on a new line. Test it with various numbers of messages.

Destructuring Assignment

Destructuring assignment is a JavaScript expression that makes it possible to unpack values from arrays or properties from objects into distinct variables.

Detailed Explanation:

  • Array Destructuring:
    • Extracts values from arrays based on their position.
    • Uses square brackets [] on the left-hand side of the assignment.
    • Can skip elements, use default values for undefined elements, and use a rest pattern to gather remaining elements.
  • Object Destructuring:
    • Extracts properties from objects based on their property names.
    • Uses curly braces {} on the left-hand side of the assignment.
    • Can rename properties, use default values for missing properties, and use a rest pattern to gather remaining properties into a new object.
    • Useful in function parameters to directly extract properties from an object passed as an argument.

Code Examples:

// Array Destructuring
const colors = ["red", "green", "blue", "yellow"];
const [firstColor, secondColor, , fourthColor] = colors; // Skipping "blue"
console.log(firstColor);  // Output: red
console.log(secondColor); // Output: green
console.log(fourthColor); // Output: yellow

// Default values with array destructuring
const [color1, color2, color3 = "purple"] = ["orange", "teal"];
console.log(color1, color2, color3); // Output: orange teal purple

// Rest pattern with array destructuring
const [head, ...tail] = colors; // 'tail' will be an array of remaining elements
console.log("Head:", head);   // Output: red
console.log("Tail:", tail);   // Output: [ 'green', 'blue', 'yellow' ]

// Object Destructuring
const person = {
  name: "Sarah",
  age: 28,
  city: "New York"
};
const {
  name,
  age
} = person; // Variables 'name' and 'age' created
console.log(`${name} is ${age} years old.`); // Output: Sarah is 28 years old.

// Renaming properties during destructuring
const {
  name: personName,
  city: residence
} = person;
console.log(`${personName} lives in ${residence}.`); // Output: Sarah lives in New York.

// Default values with object destructuring
const {
  country = "USA",
  zipCode = "90210"
} = person; // These properties don't exist in 'person'
console.log(`Country: ${country}, Zip Code: ${zipCode}`); // Output: Country: USA, Zip Code: 90210

// Rest pattern with object destructuring
const {
  city,
  ...otherDetails
} = person; // 'otherDetails' will be a new object with remaining properties
console.log("City:", city); // Output: New York
console.log("Other Details:", otherDetails); // Output: { name: 'Sarah', age: 28 }

// Destructuring in function parameters
function displayPersonInfo({
  name,
  age,
  occupation = "Unemployed"
}) { // Direct destructuring of object argument
  console.log(`${name} (${age}) - Occupation: ${occupation}`);
}

const employee = {
  name: "John",
  age: 35,
  occupation: "Engineer"
};
displayPersonInfo(employee); // Output: John (35) - Occupation: Engineer
displayPersonInfo({
  name: "Laura",
  age: 22
}); // Output: Laura (22) - Occupation: Unemployed

Exercises/Mini-Challenges:

  1. Given an array const data = ["apple", 150, true, "red"];.
    • Use array destructuring to extract the first element into itemName, the second into itemQuantity, and the last element into itemColor. Print all three variables.
  2. Given an object const product = { productName: "Laptop Pro", price: 1500, inStock: true };.
    • Use object destructuring to extract productName into a variable named nameOfProduct and price. Print nameOfProduct and price.
    • Use object destructuring to extract productName and price, and also add a default value of brand: "Generic" if brand is not present in the object. Print brand.
  3. Write a function printOrderSummary({ item, quantity, totalPrice }) that takes an object as an argument and uses object destructuring in its parameters. It should print a summary like “Order: X units of Y for $Z”. Create an order object and call the function.

Template Literals (Template Strings)

Template literals (also known as template strings) are a new way to define strings in JavaScript, introduced in ES6, offering enhanced capabilities over traditional string literals.

Detailed Explanation:

  • Multi-line Strings: Unlike single or double quotes, template literals can span multiple lines without needing special escape characters (\n).
  • Embedded Expressions: You can embed JavaScript expressions directly within the string using ${expression}. This is incredibly powerful for injecting variable values, function calls, or any valid JavaScript expression into a string.
  • Tagged Templates: A more advanced feature where you can parse template literals with a function. This allows for more complex string construction or for applying transformations to the string parts and interpolated values. (Beyond basic beginner scope, but good to be aware of).

Code Examples:

// Basic embedding of variables
const userName = "Alice";
const userAge = 30;
const greeting = `Hello, ${userName}! You are ${userAge} years old.`;
console.log(greeting); // Output: Hello, Alice! You are 30 years old.

// Multi-line strings
const multiLineMessage = `
This is a message
that spans
multiple lines.
`;
console.log(multiLineMessage);
// Output:
// This is a message
// that spans
// multiple lines.

// Embedding expressions
const price = 10;
const quantity = 3;
const total = `The total cost is $${price * quantity}.`;
console.log(total); // Output: The total cost is $30.

const isAdult = age => age >= 18 ? "an adult" : "a minor";
const statusMessage = `You are ${isAdult(userAge)}.`;
console.log(statusMessage); // Output: You are an adult.

Exercises/Mini-Challenges:

  1. Declare variables for productName, productPrice, and productQuantity.
    • Use a template literal to create a string that summarizes the product order: “You ordered X units of Y, total price: $Z”. Print this string.
  2. Create a template literal for a personalized email greeting that includes the user’s firstName, lastName, and a dynamic message based on whether their accountStatus is “active” or “inactive” (e.g., “Your account is active and ready!” or “Your account is inactive. Please log in to reactivate.”).

Classes (ES6)

Classes are a syntactic sugar over JavaScript’s existing prototype-based inheritance. They provide a cleaner, more object-oriented syntax for creating objects and dealing with inheritance.

Detailed Explanation:

  • class keyword: Used to declare a class.
  • constructor method: A special method for creating and initializing an object created with a class. It’s called automatically when a new object instance is created using new.
  • Properties: Variables associated with an instance of a class. They are typically set in the constructor using this.propertyName = value;.
  • Methods: Functions defined within a class that operate on the instance’s data.
  • extends keyword: Used to create a subclass (child class) that inherits properties and methods from a superclass (parent class).
  • super() keyword: Used in a subclass constructor to call the constructor of its superclass. Must be called before this is used in the subclass constructor.
  • static methods: Methods that belong to the class itself, not to instances of the class. They are called directly on the class name (e.g., ClassName.staticMethod()).

Code Examples:

// Define a base class
class Animal {
  constructor(name, species) {
    this.name = name;
    this.species = species;
  }

  // Method
  introduce() {
    console.log(`Hi, I'm ${this.name}, a ${this.species}.`);
  }

  // Static method
  static describe() {
    console.log("This is a generic animal class.");
  }
}

// Create an instance of Animal
const lion = new Animal("Simba", "Lion");
lion.introduce(); // Output: Hi, I'm Simba, a Lion.
Animal.describe(); // Output: This is a generic animal class.
// lion.describe(); // Error: lion.describe is not a function

// Define a subclass that extends Animal
class Dog extends Animal {
  constructor(name, breed) {
    super(name, "Dog"); // Call the parent class constructor
    this.breed = breed;
  }

  // Override a method
  introduce() {
    console.log(`Woof! My name is ${this.name}, and I'm a ${this.breed}.`);
  }

  // New method specific to Dog
  fetch() {
    console.log(`${this.name} is fetching the ball!`);
  }
}

// Create an instance of Dog
const poodle = new Dog("Puffy", "Poodle");
poodle.introduce(); // Output: Woof! My name is Puffy, and I'm a Poodle.
poodle.fetch(); // Output: Puffy is fetching the ball!

// Check if an instance is of a certain class
console.log(poodle instanceof Dog);    // Output: true
console.log(poodle instanceof Animal); // Output: true
console.log(lion instanceof Dog);      // Output: false

Exercises/Mini-Challenges:

  1. Create a class named Rectangle with a constructor that takes width and height as parameters.
    • Add a method getArea() that returns the area of the rectangle.
    • Add a method getPerimeter() that returns the perimeter.
    • Create an instance of Rectangle and call both methods, printing the results.
  2. Create a subclass named Square that extends Rectangle.
    • Its constructor should take only a sideLength and pass it appropriately to the super constructor.
    • Create an instance of Square and verify its area and perimeter methods work correctly.
  3. Add a static method to the Rectangle class called createFromArea(area, aspectRatio). This method should calculate the width and height based on the total area and a given aspectRatio (e.g., 1 for a square, 2 for width twice height) and return a new Rectangle instance. (Formula: width = sqrt(area * aspectRatio), height = sqrt(area / aspectRatio)).

4. Advanced Topics and Best Practices

Having covered the intermediate aspects, let’s dive into more complex areas of JavaScript and discuss best practices that lead to more robust, efficient, and maintainable code.

Modules (ES Modules)

Modules allow you to organize your JavaScript code into separate files, making it more manageable, reusable, and easier to debug. ES Modules (ESM), introduced in ES6, are the standardized module system in JavaScript.

Detailed Explanation:

  • export keyword: Used to make variables, functions, or classes available for use in other JavaScript files.
    • Named Exports: Export multiple values from a module.
    • Default Exports: Export a single primary value from a module. A module can only have one default export.
  • import keyword: Used to bring exported values from other modules into the current file.
    • Named Imports: Require the exact name of the exported value.
    • Default Imports: Can be given any name when imported.
    • Importing all: import * as name from 'module-name'; imports all named exports as properties of an object.
  • Advantages:
    • Modularity: Breaks code into smaller, independent chunks.
    • Reusability: Code written in one module can be reused across different parts of an application or in other projects.
    • Maintainability: Easier to understand, debug, and update specific parts of the codebase.
    • Dependency Management: Clearer dependencies between files.
    • Encapsulation: Variables and functions within a module are private by default unless explicitly exported.
  • How to use in HTML: You must add type="module" to your <script> tags when importing ES Modules in the browser.
  • Node.js: Node.js supports ES Modules, but older versions might still primarily use CommonJS (require/module.exports). In modern Node.js, you can use .mjs file extension or set "type": "module" in your package.json.

Code Examples:

Let’s create three files: math.js, utils.js, and main.js.

math.js (Module exporting named exports):

// math.js
export const PI = 3.14159;

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

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

// This function is not exported, so it's private to this module
function multiply(a, b) {
  return a * b;
}

utils.js (Module exporting a default export):

// utils.js
function capitalize(str) {
  if (typeof str !== 'string' || str.length === 0) {
    return "";
  }
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

// Default export (only one per module)
export default capitalize;

main.js (Main application file, importing from other modules):

// main.js
// Import named exports from math.js
import { PI, add } from './math.js';
// Or import all named exports as an object: import * as MathOperations from './math.js';

// Import the default export from utils.js (can be named anything)
import formatText from './utils.js'; // 'formatText' could have been 'myCapitalizeFunction'

console.log("PI:", PI); // Output: PI: 3.14159
console.log("5 + 3 =", add(5, 3)); // Output: 5 + 3 = 8

// If you imported with import * as MathOperations from './math.js';
// console.log("5 - 3 =", MathOperations.subtract(5, 3));

const name = "john doe";
console.log("Formatted name:", formatText(name)); // Output: Formatted name: John doe

// Example of not being able to access private function
// console.log(multiply(2, 2)); // Error: multiply is not defined

To run these examples in a browser, you need an index.html file:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ES Modules Example</title>
</head>
<body>
  <h1>Check the console for module output!</h1>

  <!-- Type="module" is crucial for ES Modules in the browser -->
  <script type="module" src="main.js"></script>
</body>
</html>

Save math.js, utils.js, main.js, and index.html in the same folder. Open index.html in your browser and check the console.

To run these examples in Node.js, create a package.json file in your project directory (if you don’t have one) and add "type": "module" to it:

{
  "name": "js-modules-example",
  "version": "1.0.0",
  "type": "module", // This line is crucial for Node.js to treat .js files as ES Modules
  "main": "main.js",
  "scripts": {
    "start": "node main.js"
  }
}

Then, from your terminal in the project directory, run node main.js.

Exercises/Mini-Challenges:

  1. Create a file constants.js that exports two named constants: MAX_ITEMS = 100 and APP_VERSION = "1.0.0".
  2. Create a file validation.js that exports a default function isValidEmail(email) that returns true if the email is a non-empty string and contains an “@” symbol, false otherwise.
  3. In your app.js (or main.js), import MAX_ITEMS and isValidEmail. Use console.log() to print MAX_ITEMS and test isValidEmail with a valid and invalid email address.

Object-Oriented Programming (OOP) Principles

While JavaScript is a multi-paradigm language, it heavily supports Object-Oriented Programming (OOP) principles, especially with the introduction of classes in ES6. Understanding these principles helps in designing scalable and maintainable applications.

Detailed Explanation:

  • Encapsulation: Bundling data (properties) and methods (functions) that operate on the data within a single unit (an object or class), and restricting direct access to some of an object’s components. In JavaScript, strict private properties (#property) are a newer feature, but encapsulation is primarily achieved through closures and conventions (e.g., methods that manipulate internal state).
  • Inheritance: A mechanism where a new class (subclass/child class) derives properties and behavior from an existing class (superclass/parent class). This promotes code reuse and creates a hierarchical relationship. Achieved using the extends keyword in JavaScript classes.
  • Polymorphism: The ability of objects of different classes to respond to the same method call in their own specific ways. It means “many forms.” In JavaScript, this is often seen through method overriding (a subclass providing its own implementation of a method inherited from its superclass) or simply through different objects having methods with the same name.
  • Abstraction: Hiding complex implementation details and showing only the essential features of an object. In JavaScript, this is achieved by designing methods that provide a clear interface without exposing internal complexity. There’s no built-in abstract keyword like in some other languages, but it can be simulated.

Code Examples (Building on previous Class example):

// Encapsulation example (using convention and private fields)
class BankAccount {
  #balance; // Private field (newer syntax, typically ES2022+)
  #accountNumber; // Another private field

  constructor(initialBalance, accountNumber) {
    if (initialBalance < 0) {
      throw new Error("Initial balance cannot be negative.");
    }
    this.#balance = initialBalance;
    this.#accountNumber = accountNumber;
    console.log(`Account ${this.#accountNumber} created with balance: $${this.#balance}`);
  }

  deposit(amount) {
    if (amount <= 0) {
      console.warn("Deposit amount must be positive.");
      return;
    }
    this.#balance += amount;
    console.log(`Deposited $${amount}. New balance: $${this.#balance}`);
  }

  withdraw(amount) {
    if (amount <= 0) {
      console.warn("Withdrawal amount must be positive.");
      return;
    }
    if (amount > this.#balance) {
      console.error("Insufficient funds!");
      return;
    }
    this.#balance -= amount;
    console.log(`Withdrew $${amount}. New balance: $${this.#balance}`);
  }

  // Public method to access balance (controlled access)
  getBalance() {
    return this.#balance;
  }

  getAccountNumber() {
    return this.#accountNumber;
  }
}

const myAccount = new BankAccount(100, "12345");
myAccount.deposit(50);
myAccount.withdraw(30);
// console.log(myAccount.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
console.log("Current Balance:", myAccount.getBalance()); // Access via public method

// Inheritance and Polymorphism (re-using Animal and Dog classes)
class Cat extends Animal { // Inherits from Animal
  constructor(name, favoriteToy) {
    super(name, "Cat"); // Call parent constructor
    this.favoriteToy = favoriteToy;
  }

  // Polymorphism: Cat provides its own sound (overrides implicit bark)
  makeSound() {
    console.log("Meow!");
  }

  play() {
    console.log(`${this.name} is playing with its ${this.favoriteToy}.`);
  }
}

const goldenRetriever = new Dog("Charlie", "Golden Retriever");
const siameseCat = new Cat("Whiskers", "yarn ball");

goldenRetriever.introduce(); // Dog's overridden method
goldenRetriever.fetch();
siameseCat.introduce();    // Animal's method via inheritance
siameseCat.makeSound();    // Cat's specific method
siameseCat.play();

// Polymorphism in action:
const animals = [lion, goldenRetriever, siameseCat];
animals.forEach(animal => {
  console.log("---");
  animal.introduce(); // Each animal responds in its own way
  // If the method exists, call it (example for makeSound, not all animals have it)
  if (typeof animal.makeSound === 'function') {
      animal.makeSound();
  }
});

Exercises/Mini-Challenges:

  1. Encapsulation: Modify the Rectangle class from previous exercises.
    • Make width and height properties “private” using the # syntax (e.g., #width, #height).
    • Add public getter methods getWidth() and getHeight() to access them.
    • Add public setter methods setWidth(newWidth) and setHeight(newHeight) that include basic validation (e.g., throw an error if newWidth or newHeight is negative).
    • Test accessing and setting dimensions using these new methods.
  2. Inheritance & Polymorphism:
    • Create a base class Shape with a method draw(), which simply prints “Drawing a shape.”.
    • Create subclasses Circle and Triangle that extends Shape.
    • Each subclass should have its own draw() method that prints “Drawing a Circle.” and “Drawing a Triangle.” respectively (demonstrating polymorphism by overriding).
    • Create an array of mixed Shape, Circle, and Triangle instances. Loop through the array and call the draw() method on each instance, observing the polymorphic behavior.

Functional Programming Concepts

Functional Programming (FP) is a programming paradigm where programs are constructed by applying and composing functions. It treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. While JavaScript isn’t purely functional, it has excellent support for FP principles, which can lead to more predictable and testable code.

Detailed Explanation:

  • Pure Functions:
    • Given the same inputs, they always return the same output.
    • They produce no side effects (don’t modify external state, console.log is a side effect but for pure function definition, usually refers to altering global variables, modifying arguments, etc.).
    • Benefits: Predictable, easier to test, easier to reason about.
  • Immutability:
    • Data should not be changed after it’s created. Instead of modifying existing data, create new data structures with the changes.
    • For primitive types (numbers, strings, booleans), they are inherently immutable.
    • For objects and arrays, you create copies when making changes (e.g., using spread operator ..., map, filter, slice for arrays).
    • Benefits: Prevents unintended side effects, simplifies debugging, helps with concurrent programming.
  • Higher-Order Functions (HOFs):
    • Functions that either take one or more functions as arguments, or return a function as their result.
    • Examples: map, filter, reduce, forEach, setTimeout, event listeners.
  • Function Composition:
    • Combining simple functions to build more complex ones. The output of one function becomes the input of another.
    • f(g(x)) - calling g first, then f with g’s result.

Code Examples:

// Pure Function
function addPure(a, b) {
  return a + b; // Always returns a + b, no side effects
}
console.log(addPure(2, 3)); // Output: 5
console.log(addPure(2, 3)); // Output: 5 (same output for same input)

// Impure Function (modifies global state)
let totalSum = 0;
function addImpure(a, b) {
  totalSum += (a + b); // Side effect: modifies external variable
  return a + b;
}
addImpure(2, 3);
console.log(totalSum); // Output: 5
addImpure(2, 3);
console.log(totalSum); // Output: 10 (output of totalSum depends on previous calls)

// Immutability with Arrays
const numbers = [1, 2, 3, 4];

// Add an element (immutable way)
const newNumbers = [...numbers, 5]; // Creates a new array
console.log("Original numbers:", numbers);    // Output: [1, 2, 3, 4]
console.log("New numbers:", newNumbers);      // Output: [1, 2, 3, 4, 5]

// Remove an element (immutable way)
const filteredNumbers = numbers.filter(num => num !== 3); // Creates a new array
console.log("Filtered numbers:", filteredNumbers); // Output: [1, 2, 4]

// Update an element (immutable way - e.g., double the second element)
const updatedNumbers = numbers.map((num, index) => index === 1 ? num * 2 : num);
console.log("Updated numbers:", updatedNumbers); // Output: [1, 4, 3, 4]


// Immutability with Objects
const user = {
  name: "Alice",
  age: 30
};

// Update a property (immutable way)
const updatedUser = { ...user,
  age: 31,
  city: "Wonderland"
}; // Creates a new object
console.log("Original user:", user);        // Output: { name: 'Alice', age: 30 }
console.log("Updated user:", updatedUser); // Output: { name: 'Alice', age: 31, city: 'Wonderland' }

// Higher-Order Functions (map, filter, reduce are great examples)
const products = [{
  name: "Laptop",
  price: 1200
}, {
  name: "Mouse",
  price: 25
}, {
  name: "Keyboard",
  price: 75
}];

// Get product names (map is a HOF)
const productNames = products.map(product => product.name);
console.log("Product Names:", productNames); // Output: [ 'Laptop', 'Mouse', 'Keyboard' ]

// Filter expensive products (filter is a HOF)
const expensiveProducts = products.filter(product => product.price > 100);
console.log("Expensive Products:", expensiveProducts); // Output: [ { name: 'Laptop', price: 1200 } ]

// Calculate total price (reduce is a HOF)
const totalPrice = products.reduce((acc, product) => acc + product.price, 0);
console.log("Total Price:", totalPrice); // Output: 1300

// Function Composition
const toUpperCase = str => str.toUpperCase();
const addExclamation = str => `${str}!`;
const reverseString = str => str.split('').reverse().join('');

// Compose functions manually
const transformString = str => addExclamation(toUpperCase(reverseString(str)));
console.log(transformString("hello")); // Output: OLLEH!

// Libraries like Lodash/Ramda offer utility functions for composition (e.g., _.flow, R.compose)
// const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
// const transformString = compose(addExclamation, toUpperCase, reverseString);
// console.log(transformString("world")); // Output: DLROW!

Exercises/Mini-Challenges:

  1. Pure Function: Write a pure function calculateDiscountedPrice(price, discountPercentage) that takes a product’s price and a discountPercentage (e.g., 0.10 for 10%) and returns the discounted price. Ensure it has no side effects.
  2. Immutability: Given an array const numbers = [1, 2, 3];.
    • Create a new array doubledAndAdded where each number from numbers is doubled, and then the number 7 is added to the end. (Use map and spread operator). Print doubledAndAdded and verify the original numbers array is unchanged.
  3. Higher-Order Function: Create a higher-order function repeatFunction(func, times) that takes a function func and a number times. This HOF should return a new function that, when called, executes func for the specified times.
    • Example:
      const sayHi = () => console.log("Hi!");
      const repeatHiFiveTimes = repeatFunction(sayHi, 5);
      repeatHiFiveTimes(); // Should print "Hi!" five times
      

Best Practices

Adhering to best practices is crucial for writing professional, readable, maintainable, and scalable JavaScript code.

Detailed Explanation:

  1. Use const and let over var:
    • const for variables that should not be reassigned.
    • let for variables that need to be reassigned.
    • Avoid var due to its function scoping and hoisting quirks that can lead to bugs.
  2. Strict Equality (=== and !==):
    • Always use === and !== instead of == and !=.
    • Strict equality compares both value and type, preventing unexpected type coercion issues.
  3. Meaningful Variable and Function Names:
    • Use descriptive names that indicate the purpose or content of the variable/function.
    • calculateTotalPrice is better than calc. userName is better than x.
    • Follow common naming conventions (camelCase for variables/functions, PascalCase for classes).
  4. Keep Functions Small and Focused (Single Responsibility Principle):
    • Each function should do one thing and do it well.
    • This makes functions easier to test, debug, and reuse.
  5. Avoid Global Variables (Minimize Global Scope Pollution):
    • Global variables can lead to naming conflicts and make code harder to reason about as any part of the application can modify them.
    • Use modules to encapsulate code and limit what is exposed globally.
  6. Use Comments Judiciously:
    • Comments explain why something is done, not what is done (which should be clear from the code itself).
    • Keep comments up-to-date.
  7. Format Your Code Consistently:
    • Use a consistent coding style (indentation, spacing, semicolons).
    • Tools like Prettier (code formatter) and ESLint (linter) can automate this and enforce coding standards.
  8. Handle Errors Gracefully:
    • Use try...catch for synchronous errors and .catch() or try...catch with async/await for asynchronous errors.
    • Provide meaningful error messages to aid debugging.
  9. Don’t Block the Event Loop (for Node.js/browser environments):
    • JavaScript’s single-threaded nature means long-running synchronous operations can freeze the application.
    • Use asynchronous patterns (Promises, async/await) for I/O operations and heavy computations, or consider Web Workers for CPU-intensive tasks in the browser.
    • The scheduler.yield() API (Chrome 129+) can be used to break up long tasks and prevent blocking the main thread, especially useful for complex UI updates.
  10. Validate Inputs:
    • Always validate data coming from external sources (user input, API responses) to prevent unexpected behavior and security vulnerabilities.
  11. Prioritize Readability:
    • Clean, well-structured code is easier to understand and maintain by others (and your future self).
    • Break down complex logic into smaller, named functions.
  12. Leverage Built-in Methods:
    • Familiarize yourself with array methods (map, filter, reduce), string methods, and other built-in utilities. They are often optimized and more readable than custom loops.
  13. Stay Updated:
    • JavaScript is constantly evolving. Keep an eye on new ECMAScript features and modern development practices.
    • Follow reputable blogs, official documentation (MDN Web Docs, Node.js docs, React docs), and community discussions.

Example of Good vs. Bad Practice:

Bad Practice:

// Global var, loose equality, unclear names, no error handling
var data = [1, 2, "3"];
var count = 0;

function process() {
  for (var i = 0; i < data.length; i++) {
    if (data[i] == 3) { // Loose equality
      count = data[i] + 1;
      // No proper error handling for non-numbers
    }
  }
}
process();
console.log(count); // Output: 4 (due to "3" == 3 and string concatenation if count was string)

Good Practice:

// Using const/let, strict equality, meaningful names, functional approach
const numbers = [1, 2, 3]; // Renamed for clarity, using const
let foundNumber = null; // Using let for reassignable variable

function findAndProcessNumber(arr, targetNumber) {
  for (const num of arr) { // Using for...of for array iteration
    if (num === targetNumber) { // Strict equality
      console.log(`Found and processing: ${num}`);
      // Simulate processing, e.g., return a processed value
      return num + 1;
    }
  }
  // If not found, throw a specific error
  throw new Error(`Target number ${targetNumber} not found in the array.`);
}

try {
  foundNumber = findAndProcessNumber(numbers, 3);
  console.log("Processed value:", foundNumber);
} catch (error) {
  console.error("Error during processing:", error.message);
}

try {
    findAndProcessNumber(numbers, 5); // This will throw an error
} catch (error) {
    console.error("Error during processing:", error.message);
}

Common Pitfalls:

  • Type Coercion: JavaScript’s automatic type conversion can lead to unexpected results, especially with == and !=.
  • this Context: The value of this changes based on how a function is called. Arrow functions help with this by lexically binding this.
  • Asynchronous Code Complexity: Callback Hell, unhandled Promise rejections. async/await significantly mitigates this.
  • Global Variable Overwrites: Easy to accidentally overwrite global variables if not careful. Modules help prevent this.
  • Modifying Original Arrays/Objects: Accidentally changing an original data structure when a copy was intended. Use immutable patterns (..., map, filter, slice).

5. Guided Projects

These projects will help you apply the JavaScript concepts you’ve learned in a practical context. Each project is broken down into steps, encouraging you to think and code along.

Project 1: Simple To-Do List Application (Browser-Based)

Objective: Create a basic web-based To-Do list where users can add new tasks, mark tasks as complete, and delete tasks.

Technologies Used: HTML, CSS, JavaScript (DOM Manipulation, Event Handling).

Step 1: HTML Structure (index.html)

Create a basic HTML file with an input field for new tasks, an “Add Task” button, and an unordered list (<ul>) to display the tasks.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Simple To-Do List</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>My To-Do List</h1>
        <div class="input-section">
            <input type="text" id="newTaskInput" placeholder="Add a new task...">
            <button id="addTaskBtn">Add Task</button>
        </div>
        <ul id="taskList">
            <!-- Tasks will be added here by JavaScript -->
        </ul>
    </div>
    <script src="script.js"></script>
</body>
</html>

Step 2: Basic Styling (style.css)

Add some minimal CSS to make the To-Do list visually appealing.

body {
    font-family: Arial, sans-serif;
    background-color: #f4f4f4;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    min-height: 100vh;
    margin: 20px;
}

.container {
    background-color: #fff;
    padding: 30px;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    width: 100%;
    max-width: 500px;
}

h1 {
    text-align: center;
    color: #333;
    margin-bottom: 25px;
}

.input-section {
    display: flex;
    margin-bottom: 20px;
}

#newTaskInput {
    flex-grow: 1;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 16px;
}

#addTaskBtn {
    padding: 10px 15px;
    background-color: #28a745;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    margin-left: 10px;
    font-size: 16px;
    transition: background-color 0.2s;
}

#addTaskBtn:hover {
    background-color: #218838;
}

#taskList {
    list-style: none;
    padding: 0;
}

#taskList li {
    background-color: #e9ecef;
    padding: 12px;
    margin-bottom: 10px;
    border-radius: 4px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 17px;
    color: #555;
    transition: background-color 0.2s, text-decoration 0.2s;
}

#taskList li.completed {
    background-color: #d4edda;
    text-decoration: line-through;
    color: #6c757d;
}

.task-actions {
    display: flex;
    gap: 8px;
}

.complete-btn, .delete-btn {
    background: none;
    border: none;
    font-size: 1.1em;
    cursor: pointer;
    padding: 5px;
    border-radius: 3px;
    transition: background-color 0.2s;
}

.complete-btn {
    color: #007bff;
}

.complete-btn:hover {
    background-color: #cce5ff;
}

.delete-btn {
    color: #dc3545;
}

.delete-btn:hover {
    background-color: #f8d7da;
}

Step 3: JavaScript Logic (script.js)

This is where the magic happens! We’ll use JavaScript to handle adding, completing, and deleting tasks.

// Get references to HTML elements
const newTaskInput = document.getElementById('newTaskInput');
const addTaskBtn = document.getElementById('addTaskBtn');
const taskList = document.getElementById('taskList');

// Function to create a new task list item
function createTaskElement(taskText) {
    const listItem = document.createElement('li'); // Create <li>
    listItem.textContent = taskText; // Set text content

    const taskActionsDiv = document.createElement('div');
    taskActionsDiv.classList.add('task-actions');

    const completeBtn = document.createElement('button');
    completeBtn.textContent = '✔️'; // Unicode checkmark
    completeBtn.classList.add('complete-btn');

    const deleteBtn = document.createElement('button');
    deleteBtn.textContent = '❌'; // Unicode X mark
    deleteBtn.classList.add('delete-btn');

    taskActionsDiv.appendChild(completeBtn);
    taskActionsDiv.appendChild(deleteBtn);

    listItem.appendChild(taskActionsDiv); // Append buttons to the li

    return listItem;
}

// Function to add a task
function addTask() {
    const taskText = newTaskInput.value.trim(); // Get input value and remove whitespace
    if (taskText === "") {
        alert("Task cannot be empty!");
        return;
    }

    const newTaskElement = createTaskElement(taskText);
    taskList.appendChild(newTaskElement); // Add the new task to the list

    // Clear the input field
    newTaskInput.value = "";

    // IMPORTANT: Attach event listeners to the new buttons
    attachTaskEventListeners(newTaskElement);
}

// Function to attach event listeners to a task item's buttons
function attachTaskEventListeners(taskElement) {
    const completeButton = taskElement.querySelector('.complete-btn');
    const deleteButton = taskElement.querySelector('.delete-btn');

    completeButton.addEventListener('click', () => {
        taskElement.classList.toggle('completed'); // Toggle the 'completed' class
    });

    deleteButton.addEventListener('click', () => {
        taskElement.remove(); // Remove the list item from the DOM
    });
}


// Event listener for the "Add Task" button
addTaskBtn.addEventListener('click', addTask);

// Allow adding task by pressing Enter in the input field
newTaskInput.addEventListener('keypress', (event) => {
    if (event.key === 'Enter') {
        addTask();
    }
});

// Optional: Load some initial tasks (demonstration)
const initialTasks = ["Learn JavaScript", "Build a project", "Practice daily"];
initialTasks.forEach(task => {
    const taskElement = createTaskElement(task);
    taskList.appendChild(taskElement);
    attachTaskEventListeners(taskElement); // Attach listeners to initial tasks too
});

Step-by-step guidance within script.js:

  1. Get References: Select the input, add button, and task list elements using document.getElementById().
  2. createTaskElement(taskText) function:
    • This function will be responsible for creating the HTML structure for a single task.
    • Create an <li> element.
    • Set its textContent to the taskText passed in.
    • Create a div for buttons, then create a “Complete” button (✔️) and a “Delete” button ().
    • Append these buttons to the div, and the div to the li.
    • Return the li element.
  3. addTask() function:
    • Get the value from newTaskInput.
    • Use .trim() to remove leading/trailing whitespace.
    • If the input is empty, alert the user and return.
    • Call createTaskElement() to get the new task <li>.
    • Append this <li> to the taskList (the <ul> element).
    • Clear the newTaskInput field.
    • Crucially: Call attachTaskEventListeners(newTaskElement) to ensure the newly created task’s buttons are functional.
  4. attachTaskEventListeners(taskElement) function:
    • Select the .complete-btn and .delete-btn within the specific taskElement that was just created.
    • Add a click event listener to completeButton: when clicked, toggle the class completed on the taskElement (taskElement.classList.toggle('completed')). The CSS will style completed tasks.
    • Add a click event listener to deleteButton: when clicked, use taskElement.remove() to remove the <li> from the DOM.
  5. Main Event Listeners:
    • Add a click event listener to addTaskBtn that calls the addTask function.
    • Add a keypress event listener to newTaskInput that calls addTask if the Enter key is pressed (event.key === 'Enter').
  6. Initial Tasks (Optional): Loop through initialTasks array, creating and appending each, ensuring to also attachTaskEventListeners to them.

Encouraging Independent Problem-Solving: Before looking at the script.js solution above, try to implement the addTask, complete task, and delete task functionalities on your own using the DOM manipulation and event handling concepts you’ve learned. Think about:

  • How to get input from the text field.
  • How to create new HTML elements.
  • How to add elements to the existing list.
  • How to respond to button clicks for “complete” and “delete”.
  • How to apply a CSS class when a task is completed.

Project 2: Basic Quiz Application (Browser-Based)

Objective: Create a simple multiple-choice quiz that presents questions, checks answers, and displays a final score.

Technologies Used: HTML, CSS, JavaScript (Arrays, Objects, Functions, DOM Manipulation, Event Handling, Conditionals).

Step 1: HTML Structure (quiz.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Basic JavaScript Quiz</title>
    <link rel="stylesheet" href="quiz-style.css">
</head>
<body>
    <div class="quiz-container">
        <h1>JavaScript Fundamentals Quiz</h1>
        <div id="quiz">
            <div class="question-container">
                <h2 id="question">Question Text Here</h2>
                <div class="options" id="options">
                    <!-- Options will be loaded here by JS -->
                </div>
            </div>
            <button id="submitBtn">Submit Answer</button>
            <div id="result" class="result"></div>
            <button id="restartBtn" class="hidden">Restart Quiz</button>
        </div>
        <div id="finalScore" class="final-score hidden"></div>
    </div>
    <script src="quiz-script.js"></script>
</body>
</html>

Step 2: Basic Styling (quiz-style.css)

body {
    font-family: Arial, sans-serif;
    background-color: #e0f2f7;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}

.quiz-container {
    background-color: #fff;
    padding: 30px;
    border-radius: 10px;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
    width: 90%;
    max-width: 600px;
    text-align: center;
}

h1 {
    color: #0288d1;
    margin-bottom: 25px;
}

.question-container {
    margin-bottom: 20px;
    padding: 15px;
    border: 1px solid #b3e5fc;
    border-radius: 8px;
    background-color: #e1f5fe;
}

h2#question {
    color: #333;
    font-size: 1.5em;
    margin-bottom: 15px;
}

.options {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.option-btn {
    background-color: #f0f0f0;
    border: 1px solid #ccc;
    padding: 12px 20px;
    border-radius: 6px;
    font-size: 1.1em;
    cursor: pointer;
    transition: background-color 0.2s, border-color 0.2s;
    text-align: left;
}

.option-btn:hover {
    background-color: #e0e0e0;
    border-color: #999;
}

.option-btn.selected {
    background-color: #bbdefb;
    border-color: #2196f3;
}

.option-btn.correct {
    background-color: #d4edda; /* Light green */
    border-color: #28a745; /* Darker green */
}

.option-btn.incorrect {
    background-color: #f8d7da; /* Light red */
    border-color: #dc3545; /* Darker red */
}

button {
    padding: 12px 25px;
    font-size: 1.2em;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    transition: background-color 0.2s;
    margin-top: 20px;
}

#submitBtn {
    background-color: #007bff;
    color: white;
}

#submitBtn:hover {
    background-color: #0056b3;
}

#restartBtn {
    background-color: #6c757d;
    color: white;
}

#restartBtn:hover {
    background-color: #5a6268;
}

.result {
    margin-top: 15px;
    font-size: 1.1em;
    font-weight: bold;
}

.result.correct {
    color: #28a745; /* Green */
}

.result.incorrect {
    color: #dc3545; /* Red */
}

.final-score {
    margin-top: 30px;
    font-size: 1.8em;
    color: #0277bd;
}

.hidden {
    display: none;
}

Step 3: JavaScript Logic (quiz-script.js)

const questions = [{
    question: "What does HTML stand for?",
    options: ["Hyper Text Markup Language", "Hyperlink and Text Markup Language", "Home Tool Markup Language", "Hypertext Transfer Markup Language"],
    answer: "Hyper Text Markup Language"
  },
  {
    question: "Which CSS property is used for changing the text color of an element?",
    options: ["background-color", "color", "font-color", "text-style"],
    answer: "color"
  },
  {
    question: "Which JavaScript keyword is used to declare a variable whose value can be reassigned?",
    options: ["const", "var", "let", "static"],
    answer: "let"
  },
  {
    question: "What is the result of `typeof null` in JavaScript?",
    options: ["null", "object", "undefined", "string"],
    answer: "object"
  },
  {
    question: "Which of the following is NOT a JavaScript data type?",
    options: ["Boolean", "String", "Character", "Number"],
    answer: "Character"
  },
];

let currentQuestionIndex = 0;
let score = 0;
let selectedOption = null;

// Get references to HTML elements
const questionElement = document.getElementById('question');
const optionsElement = document.getElementById('options');
const submitBtn = document.getElementById('submitBtn');
const resultElement = document.getElementById('result');
const restartBtn = document.getElementById('restartBtn');
const finalScoreElement = document.getElementById('finalScore');
const quizContainer = document.getElementById('quiz');

// Function to load a question
function loadQuestion() {
  const currentQuestion = questions[currentQuestionIndex];
  questionElement.textContent = currentQuestion.question;
  optionsElement.innerHTML = ''; // Clear previous options
  resultElement.textContent = ''; // Clear previous result message
  resultElement.classList.remove('correct', 'incorrect'); // Remove result styling
  submitBtn.classList.remove('hidden'); // Show submit button
  selectedOption = null; // Reset selected option

  currentQuestion.options.forEach((option, index) => {
    const button = document.createElement('button');
    button.textContent = option;
    button.classList.add('option-btn');
    button.setAttribute('data-index', index); // Store index for selection
    optionsElement.appendChild(button);

    button.addEventListener('click', () => {
      // Remove 'selected' from previously selected button
      const currentSelected = document.querySelector('.option-btn.selected');
      if (currentSelected) {
        currentSelected.classList.remove('selected');
      }
      // Add 'selected' to the clicked button
      button.classList.add('selected');
      selectedOption = option; // Store the text of the selected option
    });
  });
}

// Function to check the answer
function checkAnswer() {
  if (selectedOption === null) {
    alert("Please select an option before submitting!");
    return;
  }

  const currentQuestion = questions[currentQuestionIndex];
  const allOptionButtons = optionsElement.querySelectorAll('.option-btn');

  // Disable all option buttons after submission
  allOptionButtons.forEach(button => {
    button.disabled = true; // Disable interaction
    if (button.textContent === currentQuestion.answer) {
      button.classList.add('correct'); // Highlight correct answer
    } else if (button.textContent === selectedOption) {
      button.classList.add('incorrect'); // Highlight incorrect selected answer
    }
  });


  if (selectedOption === currentQuestion.answer) {
    score++;
    resultElement.textContent = "Correct!";
    resultElement.classList.add('correct');
  } else {
    resultElement.textContent = `Incorrect! The correct answer was: ${currentQuestion.answer}`;
    resultElement.classList.add('incorrect');
  }

  submitBtn.classList.add('hidden'); // Hide submit button
  // Show next question or final score
  setTimeout(() => {
    currentQuestionIndex++;
    if (currentQuestionIndex < questions.length) {
      loadQuestion();
    } else {
      displayFinalScore();
    }
  }, 1500); // Give user time to see the result
}

// Function to display the final score
function displayFinalScore() {
  quizContainer.classList.add('hidden'); // Hide the quiz
  finalScoreElement.classList.remove('hidden'); // Show final score section
  finalScoreElement.innerHTML = `
        <h2>Quiz Complete!</h2>
        <p>Your final score is: ${score} out of ${questions.length}</p>
        <button id="finalRestartBtn">Play Again</button>
    `;

  // Attach event listener to the "Play Again" button in the final score section
  document.getElementById('finalRestartBtn').addEventListener('click', restartQuiz);
}

// Function to restart the quiz
function restartQuiz() {
  currentQuestionIndex = 0;
  score = 0;
  selectedOption = null;
  quizContainer.classList.remove('hidden');
  finalScoreElement.classList.add('hidden');
  loadQuestion(); // Start over
}

// Event Listeners
submitBtn.addEventListener('click', checkAnswer);
restartBtn.addEventListener('click', restartQuiz); // This button starts hidden, revealed by JS if needed

// Initial load
loadQuestion();

Step-by-step guidance within quiz-script.js:

  1. Define Questions: Create an array of objects called questions. Each object should have:
    • question: The question text (string).
    • options: An array of strings for the multiple-choice answers.
    • answer: The correct answer (string, matching one of the options).
  2. Initialize Quiz State:
    • currentQuestionIndex = 0: Keeps track of which question is currently displayed.
    • score = 0: Stores the user’s score.
    • selectedOption = null: Stores the text of the option the user selected for the current question.
  3. Get References: Select all necessary HTML elements.
  4. loadQuestion() function:
    • Get the currentQuestion object from the questions array using currentQuestionIndex.
    • Update questionElement.textContent.
    • Clear optionsElement.innerHTML to remove old buttons.
    • Loop through currentQuestion.options:
      • For each option, create a <button> element.
      • Set its textContent to the option text.
      • Add a class option-btn.
      • Append the button to optionsElement.
      • Add a click event listener to each option-btn:
        • When clicked, remove the selected class from any previously selected button.
        • Add the selected class to the clicked button.
        • Update selectedOption with the textContent of the clicked button.
    • Reset resultElement text and classes.
    • Ensure submitBtn is visible and selectedOption is null.
  5. checkAnswer() function:
    • First, check if selectedOption is null. If so, alert the user to select an option and return.
    • Get the currentQuestion.
    • Compare selectedOption with currentQuestion.answer.
    • If correct, increment score, update resultElement.textContent to “Correct!”, and add correct class.
    • If incorrect, update resultElement.textContent to “Incorrect! The correct answer was: [answer]”, and add incorrect class.
    • Crucial: Disable all option buttons and highlight the correct/incorrect answers with classes (correct, incorrect).
    • Hide the submitBtn.
    • After a short delay (e.g., 1.5 seconds using setTimeout), increment currentQuestionIndex.
    • If there are more questions, call loadQuestion(). Otherwise, call displayFinalScore().
  6. displayFinalScore() function:
    • Hide the quizContainer.
    • Show finalScoreElement.
    • Update finalScoreElement.innerHTML to display the final score and a “Play Again” button.
    • Attach a click event listener to the “Play Again” button that calls restartQuiz().
  7. restartQuiz() function:
    • Reset currentQuestionIndex to 0, score to 0, and selectedOption to null.
    • Hide finalScoreElement and show quizContainer.
    • Call loadQuestion() to start the quiz over.
  8. Initial Call & Event Listener:
    • Call loadQuestion() once to start the quiz when the page loads.
    • Add a click event listener to submitBtn that calls checkAnswer().

Encouraging Independent Problem-Solving: Before looking at the quiz-script.js solution, try to implement:

  • Loading questions and options dynamically.
  • Handling user selection of an option.
  • Comparing the selected answer to the correct answer.
  • Updating the score.
  • Moving to the next question or ending the quiz.
  • Displaying the final score.
  • Allowing the user to restart the quiz. Pay close attention to how you manage the state of the quiz (current question, score, selected option).

6. Bonus Section: Further Learning and Resources

Congratulations on completing this JavaScript textbook! This is just the beginning of your journey. The world of JavaScript is vast and ever-evolving. Here are some resources to help you continue learning and stay updated:

  • The Odin Project: A free, open-source curriculum that teaches web development from scratch, including a very strong JavaScript path. Highly project-based.
  • freeCodeCamp.org: Offers a comprehensive, free curriculum for various development topics, including a strong focus on JavaScript. You earn certifications by completing projects.
  • Scrimba: Interactive coding tutorials where you can pause, edit, and run code directly in the browser. They have excellent JavaScript and React courses.
  • JavaScript.info: A modern JavaScript tutorial that covers everything from basics to advanced topics with clear explanations and examples. An excellent reference.
  • Udemy/Coursera (Paid Options): Search for highly-rated JavaScript courses by instructors like Maximilian Schwarzmüller, Andrew Mead, or Stephen Grider. These often offer deeper dives and structured learning paths.

Official Documentation

Blogs and Articles

  • CSS-Tricks: While focused on CSS, they often have excellent JavaScript articles, especially related to front-end development.
  • Smashing Magazine: High-quality articles on web design and development, including JavaScript.
  • Dev.to: A large community platform for developers to share articles and tutorials.
  • JavaScript Weekly / Frontend Focus (Newsletters): Stay updated with the latest in JavaScript and front-end development by subscribing to these newsletters.

YouTube Channels

Community Forums/Groups

  • Stack Overflow: The go-to place for programming questions and answers. Search for existing answers before asking your own.
  • Discord Servers: Many popular frameworks and communities have active Discord servers where you can ask questions and connect with other developers (e.g., Reactiflux, Node.js communities).
  • Reddit (r/javascript, r/webdev): Active communities for discussion and news.

Next Steps/Advanced Topics

After mastering the content in this document, consider exploring these advanced topics:

  • Modern JavaScript Frameworks/Libraries:
    • React.js: For building complex user interfaces (most popular).
    • Vue.js: A progressive framework for building user interfaces.
    • Angular: A comprehensive framework for large-scale applications.
    • Next.js / Nuxt.js / SvelteKit: Full-stack frameworks built on top of React/Vue/Svelte for server-side rendering, static site generation, and more.
  • Node.js Development:
    • Express.js: A popular web framework for building REST APIs and web applications with Node.js.
    • Databases: Learning to interact with databases (e.g., MongoDB with Mongoose, PostgreSQL with Sequelize).
    • Authentication & Authorization: Securing your Node.js applications.
  • TypeScript: A superset of JavaScript that adds static typing. It helps catch errors early and improves code maintainability, especially in large projects.
  • Web Components: Reusable custom elements that work natively in the browser.
  • Testing: Learning how to write unit, integration, and end-to-end tests for your JavaScript code (e.g., Jest, React Testing Library, Playwright).
  • Performance Optimization: Techniques for writing faster JavaScript and optimizing web page loading.
  • Design Patterns: Common, reusable solutions to recurring problems in software design.
  • Webpack/Vite (Build Tools): Understanding how modern JavaScript projects are bundled and optimized for production.
  • WebAssembly (WASM): For running high-performance code (like C++, Rust) in the browser, complementing JavaScript. Recent updates in Chrome (e.g., JavaScript Promise Integration - JSPI) allow WebAssembly apps to integrate more seamlessly with JavaScript promises.
  • Browser APIs: Deeper dives into advanced browser APIs like Fetch API, Web Storage API, Geolocation API, WebSockets, Web Workers, etc.
  • Client-Side AI: Exploring Chrome’s built-in AI APIs like the Prompt API, Summarizer API, and Language Detector API for on-device machine learning capabilities.

Keep coding, keep building, and never stop learning! The JavaScript ecosystem is dynamic, and continuous learning is key to becoming a successful developer.