Documentation

Complete guide to JavaScript programming from basics to advanced

Module 1: Introduction to JavaScript
What is JavaScript?

JavaScript is a high-level, interpreted programming language that conforms to the ECMAScript specification. It's a language that is also characterized as dynamic, weakly typed, prototype-based and multi-paradigm. JavaScript enables interactive web pages and is an essential part of web applications.

Alongside HTML and CSS, JavaScript is one of the core technologies of the World Wide Web. JavaScript supports event-driven, functional, and imperative programming styles. It has APIs for working with text, arrays, dates, regular expressions, and the DOM.

History of JavaScript

JavaScript was created by Brendan Eich in 1995 while he was working at Netscape. Originally named Mocha, then LiveScript, it was renamed JavaScript to capitalize on the popularity of Java. Despite the name, JavaScript and Java are entirely different languages.

In 1997, JavaScript was standardized as ECMAScript by Ecma International to ensure compatibility across different web browsers. Since then, multiple versions of ECMAScript have been released, with ES6 (ECMAScript 2015) being a major update that introduced many new features.

How JavaScript Works

JavaScript code is typically executed by a JavaScript engine embedded in a web browser. The most common engines include V8 (used in Chrome and Node.js), SpiderMonkey (used in Firefox), and JavaScriptCore (used in Safari).

JavaScript engines typically use a Just-In-Time (JIT) compilation approach, where the code is interpreted initially and then compiled to native machine code for better performance. The JavaScript runtime includes a call stack, heap, and a task queue for managing asynchronous operations.

Where JavaScript Runs

JavaScript primarily runs in web browsers, where it can manipulate the DOM, handle events, make network requests, and more. However, with the advent of Node.js, JavaScript can also run on servers, enabling full-stack JavaScript development.

JavaScript can also run in other environments like mobile apps (React Native, NativeScript), desktop apps (Electron), and even IoT devices.

JavaScript vs. Other Languages

JavaScript differs from many other programming languages in several ways:

  • Dynamic Typing: Variables don't have fixed types, and type checking happens at runtime
  • Prototype-based Inheritance: Objects inherit directly from other objects, rather than from classes
  • First-class Functions: Functions can be treated like any other variable
  • Single-threaded with Event Loop: JavaScript uses a single thread for execution but handles concurrency through an event loop
JavaScript Ecosystem

The JavaScript ecosystem is vast and includes:

  • Frameworks: React, Angular, Vue.js for frontend development
  • Runtime Environments: Node.js for server-side development
  • Package Managers: npm and yarn for managing dependencies
  • Build Tools: Webpack, Rollup, Parcel for bundling code
  • Testing Frameworks: Jest, Mocha, Jasmine for testing
Getting Started with JavaScript

You can start writing JavaScript right away in your browser's developer console. For more serious development, you'll want to set up a proper development environment with a code editor like Visual Studio Code and Node.js installed.

JavaScript code can be included in HTML files using script tags or in separate .js files that are linked to the HTML.



<script>
    console.log("Hello, World!");
</script>


<script src="script.js"></script>
Module 2: Variables and Data Types
Variable Declarations

JavaScript provides three ways to declare variables: var, let, and const. Each has different scoping rules and behaviors.

var

var is the traditional way to declare variables in JavaScript. It has function scope (or global scope if declared outside a function) and can be redeclared and reassigned.


var name = "John"; // Global or function-scoped
var age = 25;

// Can be redeclared
var name = "Jane";

// Can be reassigned
age = 30;
let

let was introduced in ES6 and has block scope, meaning it's only accessible within the block it's defined in. It can be reassigned but not redeclared within the same scope.


let name = "John"; // Block-scoped
let age = 25;

// Can be reassigned
name = "Jane";

// Cannot be redeclared in the same scope
// let name = "Bob"; // SyntaxError

// Different scopes
if (true) {
    let blockScoped = "I'm inside a block";
}
// console.log(blockScoped); // ReferenceError: blockScoped is not defined
const

const was also introduced in ES6 and has block scope like let. However, it cannot be reassigned after declaration. For objects and arrays, the reference cannot be changed, but the contents can be modified.


const PI = 3.14159; // Block-scoped, cannot be reassigned

// PI = 3.14; // TypeError: Assignment to constant variable

// For objects and arrays, the reference is constant
const person = { name: "John", age: 25 };
person.age = 30; // This is allowed
// person = { name: "Jane" }; // This would throw an error

const numbers = [1, 2, 3];
numbers.push(4); // This is allowed
// numbers = [5, 6, 7]; // This would throw an error
Data Types

JavaScript has two categories of data types: primitive types and reference types.

Primitive Types

Primitive types are immutable and stored directly in the variable's memory location.

  • Number: Represents both integer and floating-point numbers
  • String: Represents sequences of characters
  • Boolean: Represents logical values: true or false
  • Undefined: Represents a variable that has been declared but not assigned a value
  • Null: Represents the intentional absence of any object value
  • Symbol: Represents a unique identifier (introduced in ES6)
  • BigInt: Represents integers of arbitrary precision (introduced in ES2020)

// Number
let integer = 42;
let float = 3.14;
let scientific = 1.5e-4; // 0.00015
let hex = 0xFF; // 255
let binary = 0b1010; // 10
let octal = 0o755; // 493

// String
let singleQuotes = 'Hello';
let doubleQuotes = "World";
let template = `Hello, ${singleQuotes}!`; // Template literal

// Boolean
let isTrue = true;
let isFalse = false;

// Undefined
let undefinedVar;
console.log(undefinedVar); // undefined

// Null
let nullVar = null;
console.log(nullVar); // null

// Symbol
let symbol = Symbol('description');
let anotherSymbol = Symbol('description');
console.log(symbol === anotherSymbol); // false, symbols are always unique

// BigInt
let bigInt = 123456789012345678901234567890n;
let anotherBigInt = BigInt(123456789012345678901234567890);
Reference Types

Reference types are objects stored in memory and accessed by reference.

  • Object: A collection of key-value pairs
  • Array: An ordered collection of values
  • Function: A reusable block of code
  • Date: Represents dates and times
  • RegExp: Represents regular expressions
  • Map: A collection of key-value pairs with any type of key (ES6)
  • Set: A collection of unique values (ES6)

// Object
let person = {
    name: "John",
    age: 25,
    greet: function() {
        return `Hello, I'm ${this.name}`;
    }
};

// Array
let fruits = ["apple", "banana", "orange"];
let mixed = [1, "hello", true, null];

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

// Date
let now = new Date();
let specificDate = new Date(2023, 0, 1); // January 1, 2023

// RegExp
let pattern = /ab+c/;
let anotherPattern = new RegExp("ab+c");

// Map
let map = new Map();
map.set("name", "John");
map.set(1, "one");

// Set
let set = new Set([1, 2, 3, 3, 4]); // {1, 2, 3, 4}
Type Conversion

JavaScript can convert between types implicitly (coercion) or explicitly.


// Implicit coercion
console.log("5" + 5); // "55" (string concatenation)
console.log("5" - 5); // 0 (numeric subtraction)
console.log("5" * 5); // 25 (numeric multiplication)
console.log(true + 1); // 2
console.log(false + 1); // 1

// Explicit conversion
let str = "123";
let num = Number(str); // 123
let anotherNum = parseInt(str); // 123
let floatNum = parseFloat("123.45"); // 123.45

let bool = Boolean(1); // true
let anotherBool = Boolean(0); // false

let string = String(123); // "123"
let anotherString = 123.toString(); // "123"
Type Checking

JavaScript provides several ways to check the type of a value.


// typeof operator
console.log(typeof 42); // "number"
console.log(typeof "hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (this is a known bug in JavaScript)
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof function(){}); // "function"

// Checking for null
function isNull(value) {
    return value === null;
}

// Checking for arrays
Array.isArray([]); // true
Array.isArray({}); // false

// Checking for NaN
Number.isNaN(NaN); // true
Number.isNaN(123); // false

// More precise type checking
function getType(value) {
    return Object.prototype.toString.call(value).slice(8, -1);
}

console.log(getType([])); // "Array"
console.log(getType({})); // "Object"
console.log(getType(null)); // "Null"
console.log(getType(/regex/)); // "RegExp"
Interactive Example

Try declaring different types of variables and checking their types:

Module 3: Operators and Expressions
Arithmetic Operators

Arithmetic operators perform mathematical operations on numbers.


let a = 10, b = 3;

// Addition
console.log(a + b); // 13

// Subtraction
console.log(a - b); // 7

// Multiplication
console.log(a * b); // 30

// Division
console.log(a / b); // 3.333...

// Remainder (Modulus)
console.log(a % b); // 1

// Exponentiation
console.log(a ** b); // 1000 (10^3)

// Increment
let x = 5;
x++; // x is now 6
console.log(x++); // Returns 6, then increments to 7
console.log(++x); // Increments to 8, then returns 8

// Decrement
let y = 5;
y--; // y is now 4
console.log(y--); // Returns 4, then decrements to 3
console.log(--y); // Decrements to 2, then returns 2
Assignment Operators

Assignment operators assign values to variables.


let x = 10;

// Simple assignment
x = 5;

// Addition assignment
x += 3; // x = x + 3

// Subtraction assignment
x -= 2; // x = x - 2

// Multiplication assignment
x *= 4; // x = x * 4

// Division assignment
x /= 2; // x = x / 2

// Remainder assignment
x %= 3; // x = x % 3

// Exponentiation assignment
x **= 2; // x = x ** 2

// Left shift assignment
x <<= 2; // x = x << 2

// Right shift assignment
x >>= 2; // x = x >> 2

// Unsigned right shift assignment
x >>>= 2; // x = x >>> 2

// Bitwise AND assignment
x &= 3; // x = x & 3

// Bitwise OR assignment
x |= 3; // x = x | 3

// Bitwise XOR assignment
x ^= 3; // x = x ^ 3

// Logical nullish assignment (ES2020)
let user = {};
user.name ??= "Anonymous"; // Only assigns if user.name is null or undefined
Comparison Operators

Comparison operators compare two values and return a boolean result.


// Equality (loose)
console.log(5 == "5"); // true (type coercion)
console.log(0 == false); // true
console.log(null == undefined); // true

// Inequality (loose)
console.log(5 != "5"); // false
console.log(0 != false); // false

// Strict equality
console.log(5 === "5"); // false (different types)
console.log(0 === false); // false

// Strict inequality
console.log(5 !== "5"); // true
console.log(0 !== false); // true

// Greater than
console.log(5 > 3); // true

// Greater than or equal to
console.log(5 >= 5); // true

// Less than
console.log(5 < 3); // false

// Less than or equal to
console.log(5 <= 5); // true
Logical Operators

Logical operators are used to combine conditional statements.


let a = true, b = false;

// Logical AND (&&)
console.log(a && b); // false
console.log(true && true); // true
console.log(true && false); // false
console.log(false && false); // false

// Short-circuiting with &&
let x = 5;
let y = x > 0 && "Positive"; // "Positive" (x > 0 is true, so the second operand is returned)
let z = x < 0 && "Negative"; // false (x < 0 is false, so false is returned immediately)

// Logical OR (||)
console.log(a || b); // true
console.log(true || true); // true
console.log(true || false); // true
console.log(false || false); // false

// Short-circuiting with ||
let name = null || "Anonymous"; // "Anonymous" (null is falsy, so the second operand is returned)
let age = 25 || 0; // 25 (25 is truthy, so it's returned immediately)

// Logical NOT (!)
console.log(!a); // false
console.log(!b); // true
console.log(!0); // true
console.log(!""); // true
console.log(!null); // true
console.log(!undefined); // true

// Nullish coalescing operator (??) (ES2020)
let user = { name: null, age: 0 };
let userName = user.name ?? "Anonymous"; // "Anonymous" (only null or undefined trigger the default)
let userAge = user.age ?? 18; // 0 (0 is not null or undefined, so it's used)
Bitwise Operators

Bitwise operators work on 32-bit binary representations of numbers.


// Bitwise AND (&)
console.log(5 & 3); // 1 (0101 & 0011 = 0001)

// Bitwise OR (|)
console.log(5 | 3); // 7 (0101 | 0011 = 0111)

// Bitwise XOR (^)
console.log(5 ^ 3); // 6 (0101 ^ 0011 = 0110)

// Bitwise NOT (~)
console.log(~5); // -6 (~0101 = 1010 in two's complement)

// Left shift (<<)
console.log(5 << 2); // 20 (0101 << 2 = 010100)

// Right shift (>>)
console.log(5 >> 2); // 1 (0101 >> 2 = 0001)

// Unsigned right shift (>>>)
console.log(-5 >>> 1); // 2147483645 (fills with zeros instead of the sign bit)
String Operators

The + operator is used for string concatenation. The += operator can also be used to concatenate strings.


let firstName = "John";
let lastName = "Doe";

// String concatenation
let fullName = firstName + " " + lastName; // "John Doe"

// Concatenation assignment
let greeting = "Hello";
greeting += ", world!"; // "Hello, world!"

// Template literals (ES6)
let message = `Hello, ${firstName} ${lastName}!`; // "Hello, John Doe!"

// String methods
console.log(fullName.length); // 8
console.log(fullName.toUpperCase()); // "JOHN DOE"
console.log(fullName.substring(0, 4)); // "John"
Ternary Operator

The ternary operator is a shorthand for an if-else statement.


let age = 18;
let canVote = age >= 18 ? "Yes" : "No"; // "Yes"

// Equivalent to:
let canVote2;
if (age >= 18) {
    canVote2 = "Yes";
} else {
    canVote2 = "No";
}

// Nested ternary operators
let score = 85;
let grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F"; // "B"
Operator Precedence

Operator precedence determines the order in which operations are performed. Operators with higher precedence are performed first.


// Example of operator precedence
let result = 2 + 3 * 4; // 14 (multiplication has higher precedence than addition)

// Using parentheses to change precedence
let result2 = (2 + 3) * 4; // 20

// Complex expression with multiple operators
let complex = 2 + 3 * 4 ** 2 / (5 - 1); // 2 + 3 * 16 / 4 = 2 + 48 / 4 = 2 + 12 = 14
Interactive Example

Try different JavaScript operators:

Module 4: Control Flow and Loops
Conditional Statements

Conditional statements allow you to execute different code blocks based on different conditions.

if Statement

The if statement executes a block of code if a specified condition is true.


let age = 18;

if (age >= 18) {
    console.log("You are an adult");
}
if...else Statement

The if...else statement executes one block of code if a condition is true, and another block if the condition is false.


let age = 16;

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

The if...else if...else statement allows you to check multiple conditions.


let age = 25;

if (age < 13) {
    console.log("You are a child");
} else if (age < 18) {
    console.log("You are a teenager");
} else if (age < 65) {
    console.log("You are an adult");
} else {
    console.log("You are a senior");
}
switch Statement

The switch statement evaluates an expression and executes the corresponding case block.


let day = "Monday";

switch (day) {
    case "Monday":
        console.log("Start of the work week");
        break;
    case "Tuesday":
    case "Wednesday":
    case "Thursday":
        console.log("Middle of the work week");
        break;
    case "Friday":
        console.log("End of the work week");
        break;
    case "Saturday":
    case "Sunday":
        console.log("Weekend");
        break;
    default:
        console.log("Invalid day");
}
Loops

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

for Loop

The for loop repeats a block of code a specified number of times.


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

// Looping through an array
let fruits = ["apple", "banana", "orange"];
for (let i = 0; i < fruits.length; i++) {
    console.log(fruits[i]);
}
while Loop

The while loop repeats a block of code as long as a specified condition is true.


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

The do...while loop repeats a block of code once, and then repeats as long as a specified condition is true.


let i = 0;
do {
    console.log(i); // 0, 1, 2, 3, 4
    i++;
} while (i < 5);
for...in Loop

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


let person = {
    name: "John",
    age: 25,
    occupation: "Developer"
};

for (let key in person) {
    console.log(key + ": " + person[key]);
}
// Output:
// name: John
// age: 25
// occupation: Developer
for...of Loop

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


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

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

// Iterating over a string
let str = "Hello";
for (let char of str) {
    console.log(char);
}
// Output:
// H
// e
// l
// l
// o
Loop Control Statements

Loop control statements change the normal execution of a loop.

break Statement

The break statement exits a loop immediately.


for (let i = 0; i < 10; i++) {
    if (i === 5) {
        break;
    }
    console.log(i); // 0, 1, 2, 3, 4
}
continue Statement

The continue statement skips the current iteration of a loop and continues with the next iteration.


for (let i = 0; i < 10; i++) {
    if (i === 5) {
        continue;
    }
    console.log(i); // 0, 1, 2, 3, 4, 6, 7, 8, 9
}
Label Statement

A label can be used with break or continue to control nested loops.


outerLoop: for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
        if (i === 1 && j === 1) {
            break outerLoop; // Breaks out of the outer loop
        }
        console.log(`i: ${i}, j: ${j}`);
    }
}
// Output:
// i: 0, j: 0
// i: 0, j: 1
// i: 0, j: 2
// i: 1, j: 0
Interactive Example

Try different control flow structures:

Module 5: Functions and Scope
Function Declarations

Functions are reusable blocks of code that perform specific tasks. JavaScript provides several ways to define functions.

Function Declaration

A function declaration defines a function with the specified parameters.


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

console.log(greet("John")); // "Hello, John!"
Function Expression

A function expression defines a function as part of a larger expression syntax.


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

console.log(greet("John")); // "Hello, John!"
Arrow Function

Arrow functions provide a concise syntax for writing function expressions.


// Arrow function with one parameter and one statement
const greet = name => "Hello, " + name + "!";

// Arrow function with multiple parameters
const add = (a, b) => a + b;

// Arrow function with multiple statements
const calculate = (a, b, operation) => {
    let result;
    switch (operation) {
        case "add":
            result = a + b;
            break;
        case "subtract":
            result = a - b;
            break;
        case "multiply":
            result = a * b;
            break;
        case "divide":
            result = a / b;
            break;
        default:
            result = "Invalid operation";
    }
    return result;
};

console.log(greet("John")); // "Hello, John!"
console.log(add(5, 3)); // 8
console.log(calculate(5, 3, "multiply")); // 15
Immediately Invoked Function Expression (IIFE)

An IIFE is a function that runs as soon as it is defined.


(function() {
    console.log("This runs immediately!");
})();

// IIFE with parameters
(function(name) {
    console.log("Hello, " + name + "!");
})("John");
Function Parameters

Functions can take parameters, which are values passed into the function.

Default Parameters

Default parameters allow named parameters to be initialized with default values if no value or undefined is passed.


function greet(name = "Guest") {
    return "Hello, " + name + "!";
}

console.log(greet()); // "Hello, Guest!"
console.log(greet("John")); // "Hello, John!"
Rest Parameters

Rest parameters allow an indefinite number of arguments as an array.


function sum(...numbers) {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
Arguments Object

The arguments object is an array-like object containing the values of the arguments passed to a function.


function sum() {
    let total = 0;
    for (let i = 0; i < arguments.length; i++) {
        total += arguments[i];
    }
    return total;
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
Function Scope

Scope determines the accessibility of variables, functions, and objects in different parts of the code.

Global Scope

Variables declared outside of any function have global scope and can be accessed from anywhere in the code.


var globalVar = "I'm global";

function showGlobal() {
    console.log(globalVar); // "I'm global"
}

showGlobal();
console.log(globalVar); // "I'm global"
Function Scope

Variables declared inside a function have function scope and can only be accessed within that function.


function showScope() {
    var localVar = "I'm local";
    console.log(localVar); // "I'm local"
}

showScope();
// console.log(localVar); // ReferenceError: localVar is not defined
Block Scope

Variables declared with let and const have block scope, meaning they are only accessible within the block they are defined in.


function showBlockScope() {
    if (true) {
        let blockVar = "I'm block-scoped";
        console.log(blockVar); // "I'm block-scoped"
    }
    // console.log(blockVar); // ReferenceError: blockVar is not defined
}

showBlockScope();
Closures

A closure is a function that has access to variables in its outer (enclosing) scope even after the outer function has returned.


function outerFunction(x) {
    // Outer function variable
    return function(y) {
        // Inner function has access to x
        return x + y;
    };
}

const addFive = outerFunction(5);
console.log(addFive(3)); // 8

// Practical example: counter
function createCounter() {
    let count = 0;
    return {
        increment: function() {
            count++;
            return count;
        },
        decrement: function() {
            count--;
            return count;
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
console.log(counter.decrement()); // 1
Higher-Order Functions

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


// Function that takes another function as an argument
function calculate(operation, a, b) {
    return operation(a, b);
}

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

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

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

// Function that returns another function
function createMultiplier(factor) {
    return function(number) {
        return number * factor;
    };
}

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

console.log(double(5)); // 10
console.log(triple(5)); // 15
Recursive Functions

Recursive functions call themselves to solve problems by breaking them down into smaller subproblems.


// Factorial using recursion
function factorial(n) {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

console.log(factorial(5)); // 120

// Fibonacci using recursion
function fibonacci(n) {
    if (n <= 1) {
        return n;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(fibonacci(10)); // 55
Interactive Example

Try defining and calling a function:

Module 6: Objects and Prototypes
Creating Objects

Objects are collections of key-value pairs. JavaScript provides several ways to create objects.

Object Literal

The simplest way to create an object is using an object literal.


let person = {
    name: "John",
    age: 25,
    greet: function() {
        return "Hello, I'm " + this.name;
    }
};

console.log(person.name); // "John"
console.log(person.greet()); // "Hello, I'm John"
Object Constructor

The Object constructor creates an empty object that can be populated with properties.


let person = new Object();
person.name = "John";
person.age = 25;
person.greet = function() {
    return "Hello, I'm " + this.name;
};

console.log(person.name); // "John"
console.log(person.greet()); // "Hello, I'm John"
Constructor Function

A constructor function is used to create multiple objects of the same type.


function Person(name, age) {
    this.name = name;
    this.age = age;
    this.greet = function() {
        return "Hello, I'm " + this.name;
    };
}

let person1 = new Person("John", 25);
let person2 = new Person("Jane", 30);

console.log(person1.name); // "John"
console.log(person2.name); // "Jane"
console.log(person1.greet()); // "Hello, I'm John"
Object.create Method

The Object.create method creates a new object with the specified prototype object.


let personProto = {
    greet: function() {
        return "Hello, I'm " + this.name;
    }
};

let person = Object.create(personProto);
person.name = "John";
person.age = 25;

console.log(person.name); // "John"
console.log(person.greet()); // "Hello, I'm John"
ES6 Class

ES6 introduced class syntax for creating objects, which is syntactic sugar over JavaScript's prototype-based inheritance.


class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        return "Hello, I'm " + this.name;
    }
}

let person = new Person("John", 25);
console.log(person.name); // "John"
console.log(person.greet()); // "Hello, I'm John"
Object Properties

Object properties are key-value pairs associated with an object.

Accessing Properties

Properties can be accessed using dot notation or bracket notation.


let person = {
    name: "John",
    age: 25
};

// Dot notation
console.log(person.name); // "John"

// Bracket notation
console.log(person["age"]); // 25

// Bracket notation with variable
let property = "name";
console.log(person[property]); // "John"
Adding and Modifying Properties

Properties can be added or modified using dot notation or bracket notation.


let person = {
    name: "John",
    age: 25
};

// Adding a property
person.email = "john@example.com";

// Modifying a property
person.age = 26;

// Using bracket notation
person["city"] = "New York";
person["age"] = 27;

console.log(person);
// {
//   name: "John",
//   age: 27,
//   email: "john@example.com",
//   city: "New York"
// }
Deleting Properties

The delete operator removes a property from an object.


let person = {
    name: "John",
    age: 25,
    email: "john@example.com"
};

delete person.email;
console.log(person.email); // undefined
console.log("email" in person); // false
Property Descriptors

Property descriptors provide information about a property and can be used to define new properties with specific attributes.


let person = {};

// Define a property with descriptors
Object.defineProperty(person, "name", {
    value: "John",
    writable: false, // Cannot be changed
    enumerable: true, // Will show up in for...in loops
    configurable: false // Cannot be deleted or reconfigured
});

console.log(person.name); // "John"
person.name = "Jane"; // Won't change because writable is false
console.log(person.name); // "John"

// Get property descriptor
let descriptor = Object.getOwnPropertyDescriptor(person, "name");
console.log(descriptor);
// {
//   value: "John",
//   writable: false,
//   enumerable: true,
//   configurable: false
// }

// Define multiple properties
Object.defineProperties(person, {
    age: {
        value: 25,
        writable: true,
        enumerable: true,
        configurable: true
    },
    email: {
        value: "john@example.com",
        writable: true,
        enumerable: true,
        configurable: true
    }
});
Object Methods

JavaScript provides several built-in methods for working with objects.

Object.keys

The Object.keys method returns an array of a given object's own enumerable property names.


let person = {
    name: "John",
    age: 25,
    email: "john@example.com"
};

let keys = Object.keys(person);
console.log(keys); // ["name", "age", "email"]
Object.values

The Object.values method returns an array of a given object's own enumerable property values.


let person = {
    name: "John",
    age: 25,
    email: "john@example.com"
};

let values = Object.values(person);
console.log(values); // ["John", 25, "john@example.com"]
Object.entries

The Object.entries method returns an array of a given object's own enumerable string-keyed property [key, value] pairs.


let person = {
    name: "John",
    age: 25,
    email: "john@example.com"
};

let entries = Object.entries(person);
console.log(entries);
// [
//   ["name", "John"],
//   ["age", 25],
//   ["email", "john@example.com"]
// ]
Object.assign

The Object.assign method copies all enumerable own properties from one or more source objects to a target object.


let target = { a: 1, b: 2 };
let source = { b: 3, c: 4 };

let result = Object.assign(target, source);
console.log(result); // { a: 1, b: 3, c: 4 }

// Creating a new object
let person = { name: "John" };
let details = { age: 25, email: "john@example.com" };
let fullPerson = Object.assign({}, person, details);
console.log(fullPerson); // { name: "John", age: 25, email: "john@example.com" }
Prototypes and Inheritance

JavaScript uses prototypal inheritance, where objects can inherit properties from other objects.

Prototype Chain

Every object has a prototype, which is another object from which it inherits properties.


function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.greet = function() {
    return "Hello, I'm " + this.name;
};

let person = new Person("John", 25);
console.log(person.greet()); // "Hello, I'm John"

// Checking prototype
console.log(Object.getPrototypeOf(person) === Person.prototype); // true
console.log(person.__proto__ === Person.prototype); // true
Prototype Inheritance

Objects can inherit from other objects through the prototype chain.


function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.greet = function() {
    return "Hello, I'm " + this.name;
};

function Student(name, age, grade) {
    Person.call(this, name, age); // Call the parent constructor
    this.grade = grade;
}

// Inherit from Person
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

// Override the greet method
Student.prototype.greet = function() {
    return Person.prototype.greet.call(this) + " and I'm a student in grade " + this.grade;
};

let student = new Student("John", 15, 10);
console.log(student.greet()); // "Hello, I'm John and I'm a student in grade 10"
Object Destructuring

Object destructuring allows you to extract properties from objects into distinct variables.


let person = {
    name: "John",
    age: 25,
    email: "john@example.com"
};

// Basic destructuring
let { name, age } = person;
console.log(name); // "John"
console.log(age); // 25

// Destructuring with different variable names
let { name: personName, age: personAge } = person;
console.log(personName); // "John"
console.log(personAge); // 25

// Destructuring with default values
let { name, city = "Unknown" } = person;
console.log(name); // "John"
console.log(city); // "Unknown"

// Destructuring nested objects
let user = {
    profile: {
        name: "John",
        age: 25
    },
    contact: {
        email: "john@example.com",
        phone: "123-456-7890"
    }
};

let { profile: { name, age }, contact: { email } } = user;
console.log(name); // "John"
console.log(age); // 25
console.log(email); // "john@example.com"
Interactive Example

Try creating and manipulating an object:

Module 7: Arrays and Array Methods
Creating Arrays

Arrays are ordered collections of values. JavaScript provides several ways to create arrays.

Array Literal

The simplest way to create an array is using an array literal.


let fruits = ["apple", "banana", "orange"];
let numbers = [1, 2, 3, 4, 5];
let mixed = [1, "hello", true, null, undefined];
let empty = [];
Array Constructor

The Array constructor creates an array with the specified elements.


let fruits = new Array("apple", "banana", "orange");
let numbers = new Array(1, 2, 3, 4, 5);

// Creating an array with a specific length
let empty = new Array(5); // Creates an array with 5 empty slots
console.log(empty.length); // 5
Array.from Method

The Array.from method creates a new array from an array-like or iterable object.


// Create an array from a string
let str = "hello";
let chars = Array.from(str);
console.log(chars); // ["h", "e", "l", "l", "o"]

// Create an array with a mapping function
let numbers = Array.from({length: 5}, (value, index) => index + 1);
console.log(numbers); // [1, 2, 3, 4, 5]
Array.of Method

The Array.of method creates a new array with the provided arguments, regardless of the number or type of arguments.


let numbers = Array.of(1, 2, 3, 4, 5);
console.log(numbers); // [1, 2, 3, 4, 5]

// Unlike the Array constructor, Array.of always creates an array with the provided elements
let single = Array.of(5);
console.log(single); // [5]

let withConstructor = new Array(5);
console.log(withConstructor); // [empty × 5]
Accessing Array Elements

Array elements can be accessed using their index, which starts at 0.


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

// Accessing elements
console.log(fruits[0]); // "apple"
console.log(fruits[1]); // "banana"
console.log(fruits[2]); // "orange"

// Accessing the last element
console.log(fruits[fruits.length - 1]); // "orange"

// Accessing elements that don't exist
console.log(fruits[3]); // undefined
Modifying Arrays

Arrays can be modified by adding, removing, or changing elements.

Adding Elements

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

// Adding to the end
fruits.push("grape");
console.log(fruits); // ["apple", "banana", "orange", "grape"]

// Adding to the beginning
fruits.unshift("strawberry");
console.log(fruits); // ["strawberry", "apple", "banana", "orange", "grape"]

// Adding at a specific position
fruits.splice(2, 0, "kiwi");
console.log(fruits); // ["strawberry", "apple", "kiwi", "banana", "orange", "grape"]
Removing Elements

let fruits = ["strawberry", "apple", "kiwi", "banana", "orange", "grape"];

// Removing from the end
let last = fruits.pop();
console.log(last); // "grape"
console.log(fruits); // ["strawberry", "apple", "kiwi", "banana", "orange"]

// Removing from the beginning
let first = fruits.shift();
console.log(first); // "strawberry"
console.log(fruits); // ["apple", "kiwi", "banana", "orange"]

// Removing from a specific position
let removed = fruits.splice(1, 1);
console.log(removed); // ["kiwi"]
console.log(fruits); // ["apple", "banana", "orange"]
Changing Elements

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

// Changing an element
fruits[1] = "kiwi";
console.log(fruits); // ["apple", "kiwi", "orange"]

// Changing multiple elements
fruits.splice(1, 2, "grape", "strawberry");
console.log(fruits); // ["apple", "grape", "strawberry"]
Array Methods

JavaScript provides many built-in methods for working with arrays.

Iteration Methods

let numbers = [1, 2, 3, 4, 5];

// forEach - executes a function for each array element
numbers.forEach(num => console.log(num));
// Output:
// 1
// 2
// 3
// 4
// 5

// map - creates a new array with the result of calling a function for each array element
let doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter - creates a new array with all elements that pass a test
let evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4]

// reduce - reduces the array to a single value
let sum = numbers.reduce((total, num) => total + num, 0);
console.log(sum); // 15

// find - returns the first element that satisfies a test
let found = numbers.find(num => num > 3);
console.log(found); // 4

// findIndex - returns the index of the first element that satisfies a test
let index = numbers.findIndex(num => num > 3);
console.log(index); // 3

// some - checks if at least one element satisfies a test
let hasEven = numbers.some(num => num % 2 === 0);
console.log(hasEven); // true

// every - checks if all elements satisfy a test
let allPositive = numbers.every(num => num > 0);
console.log(allPositive); // true
Transformation Methods

let numbers = [1, 2, 3, 4, 5];

// concat - merges two or more arrays
let moreNumbers = [6, 7, 8];
let allNumbers = numbers.concat(moreNumbers);
console.log(allNumbers); // [1, 2, 3, 4, 5, 6, 7, 8]

// slice - extracts a section of an array and returns a new array
let sliced = numbers.slice(1, 4);
console.log(sliced); // [2, 3, 4]

// splice - adds or removes elements from an array
let spliced = [...numbers];
spliced.splice(1, 2, 10, 20);
console.log(spliced); // [1, 10, 20, 4, 5]

// join - joins all elements of an array into a string
let joined = numbers.join("-");
console.log(joined); // "1-2-3-4-5"

// reverse - reverses the order of the elements in an array
let reversed = [...numbers].reverse();
console.log(reversed); // [5, 4, 3, 2, 1]

// sort - sorts the elements of an array
let unsorted = [3, 1, 4, 1, 5, 9, 2, 6];
let sorted = [...unsorted].sort();
console.log(sorted); // [1, 1, 2, 3, 4, 5, 6, 9]

// Custom sort
let numbersToSort = [10, 5, 40, 25];
let customSorted = [...numbersToSort].sort((a, b) => a - b);
console.log(customSorted); // [5, 10, 25, 40]
Search Methods

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

// indexOf - returns the first index at which a given element can be found
console.log(fruits.indexOf("orange")); // 2
console.log(fruits.indexOf("pear")); // -1 (not found)

// lastIndexOf - returns the last index at which a given element can be found
let duplicated = ["apple", "banana", "apple", "orange"];
console.log(duplicated.lastIndexOf("apple")); // 2

// includes - determines whether an array includes a certain value
console.log(fruits.includes("orange")); // true
console.log(fruits.includes("pear")); // false
Array Destructuring

Array destructuring allows you to extract values from arrays into distinct variables.


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

// Basic destructuring
let [first, second, third] = fruits;
console.log(first); // "apple"
console.log(second); // "banana"
console.log(third); // "orange"

// Skipping elements
let [first, , third] = fruits;
console.log(first); // "apple"
console.log(third); // "orange"

// Using rest operator
let [first, ...rest] = fruits;
console.log(first); // "apple"
console.log(rest); // ["banana", "orange", "grape"]

// Setting default values
let [first, second, third, fourth, fifth = "pear"] = fruits;
console.log(fifth); // "pear"

// Swapping variables
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a); // 2
console.log(b); // 1
Spread Operator with Arrays

The spread operator (...) allows an iterable to be expanded in places where zero or more arguments or elements are expected.


// Creating a new array with additional elements
let fruits = ["apple", "banana", "orange"];
let moreFruits = [...fruits, "grape", "strawberry"];
console.log(moreFruits); // ["apple", "banana", "orange", "grape", "strawberry"]

// Combining arrays
let vegetables = ["carrot", "broccoli"];
let food = [...fruits, ...vegetables];
console.log(food); // ["apple", "banana", "orange", "carrot", "broccoli"]

// Copying an array
let fruitsCopy = [...fruits];
console.log(fruitsCopy); // ["apple", "banana", "orange"]

// Converting a NodeList to an array
let divs = document.querySelectorAll("div");
let divsArray = [...divs];

// Using with function arguments
function sum(a, b, c) {
    return a + b + c;
}
let numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6
Interactive Example

Try different array methods:

Module 8: ES6+ Features
Introduction to ES6+

ES6 (ECMAScript 2015) and later versions introduced many new features to JavaScript, making the language more powerful and easier to work with. These features include new syntax, new methods, and improvements to existing functionality.

let and const

ES6 introduced two new ways to declare variables: let and const. Both have block scope, unlike var which has function scope.


// let - can be reassigned
let name = "John";
name = "Jane";
console.log(name); // "Jane"

// const - cannot be reassigned
const PI = 3.14159;
// PI = 3.14; // TypeError: Assignment to constant variable

// Block scope
if (true) {
    let blockScoped = "I'm block-scoped";
    const constBlockScoped = "I'm also block-scoped";
}
// console.log(blockScoped); // ReferenceError: blockScoped is not defined
// console.log(constBlockScoped); // ReferenceError: constBlockScoped is not defined
Arrow Functions

Arrow functions provide a concise syntax for writing function expressions. They also have lexical this binding, which means they inherit this from the enclosing scope.


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

// Arrow function
const addArrow = (a, b) => a + b;

// Arrow function with one parameter
const greet = name => `Hello, ${name}!`;

// Arrow function with multiple statements
const calculate = (a, b, operation) => {
    let result;
    switch (operation) {
        case "add":
            result = a + b;
            break;
        case "subtract":
            result = a - b;
            break;
        default:
            result = "Invalid operation";
    }
    return result;
};

// Lexical this binding
function Person() {
    this.age = 0;
    
    // Traditional function - this refers to the global object or undefined in strict mode
    setInterval(function() {
        // this.age++; // This won't work as expected
    }, 1000);
    
    // Arrow function - this refers to the Person instance
    setInterval(() => {
        this.age++;
    }, 1000);
}
Template Literals

Template literals are string literals allowing embedded expressions. They are enclosed by backticks (`) instead of single or double quotes.


// Traditional string concatenation
let name = "John";
let age = 25;
let message = "My name is " + name + " and I am " + age + " years old.";

// Template literal
let templateMessage = `My name is ${name} and I am ${age} years old.`;

// Multi-line strings
let multiLine = `This is a
multi-line
string.`;

// Expressions in template literals
let price = 19.99;
let tax = 0.07;
let total = `Total: $${(price * (1 + tax)).toFixed(2)}`;
console.log(total); // "Total: $21.39"
Destructuring

Destructuring allows you to extract values from arrays or objects into distinct variables.

Array Destructuring

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

// Basic destructuring
let [first, second, third] = fruits;
console.log(first); // "apple"
console.log(second); // "banana"
console.log(third); // "orange"

// Skipping elements
let [first, , third] = fruits;
console.log(first); // "apple"
console.log(third); // "orange"

// Using rest operator
let [first, ...rest] = fruits;
console.log(first); // "apple"
console.log(rest); // ["banana", "orange", "grape"]

// Setting default values
let [first, second, third, fourth, fifth = "pear"] = fruits;
console.log(fifth); // "pear"
Object Destructuring

let person = {
    name: "John",
    age: 25,
    email: "john@example.com"
};

// Basic destructuring
let { name, age } = person;
console.log(name); // "John"
console.log(age); // 25

// Destructuring with different variable names
let { name: personName, age: personAge } = person;
console.log(personName); // "John"
console.log(personAge); // 25

// Destructuring with default values
let { name, city = "Unknown" } = person;
console.log(name); // "John"
console.log(city); // "Unknown"

// Destructuring nested objects
let user = {
    profile: {
        name: "John",
        age: 25
    },
    contact: {
        email: "john@example.com",
        phone: "123-456-7890"
    }
};

let { profile: { name, age }, contact: { email } } = user;
console.log(name); // "John"
console.log(age); // 25
console.log(email); // "john@example.com"
Default Parameters

Default parameters allow named parameters to be initialized with default values if no value or undefined is passed.


// Traditional way to set default values
function greet(name) {
    name = name || "Guest";
    return "Hello, " + name + "!";
}

// With default parameters
function greetWithDefault(name = "Guest") {
    return `Hello, ${name}!`;
}

console.log(greetWithDefault()); // "Hello, Guest!"
console.log(greetWithDefault("John")); // "Hello, John!"

// Default parameters with expressions
function createBooking(flightNumber, passengers = 1, price = 199 * passengers) {
    return {
        flightNumber,
        passengers,
        price
    };
}

console.log(createBooking("LH123")); // { flightNumber: "LH123", passengers: 1, price: 199 }
console.log(createBooking("LH123", 2)); // { flightNumber: "LH123", passengers: 2, price: 398 }
Rest and Spread Operators

The rest and spread operators both use the same syntax (...) but have different uses.

Rest Parameters

Rest parameters allow an indefinite number of arguments as an array.


// Traditional way using arguments object
function sum() {
    let total = 0;
    for (let i = 0; i < arguments.length; i++) {
        total += arguments[i];
    }
    return total;
}

// With rest parameters
function sumWithRest(...numbers) {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sumWithRest(1, 2, 3)); // 6
console.log(sumWithRest(1, 2, 3, 4, 5)); // 15

// Rest parameter must be the last parameter
function createPerson(name, ...details) {
    return {
        name,
        details
    };
}

console.log(createPerson("John", 25, "Developer", "New York"));
// { name: "John", details: [25, "Developer", "New York"] }
Spread Operator

The spread operator allows an iterable to be expanded in places where zero or more arguments or elements are expected.


// Spreading arrays
let fruits = ["apple", "banana", "orange"];
let moreFruits = [...fruits, "grape", "strawberry"];
console.log(moreFruits); // ["apple", "banana", "orange", "grape", "strawberry"]

// Combining arrays
let vegetables = ["carrot", "broccoli"];
let food = [...fruits, ...vegetables];
console.log(food); // ["apple", "banana", "orange", "carrot", "broccoli"]

// Copying an array
let fruitsCopy = [...fruits];
console.log(fruitsCopy); // ["apple", "banana", "orange"]

// Spreading objects
let person = {
    name: "John",
    age: 25
};

let personWithJob = {
    ...person,
    job: "Developer"
};
console.log(personWithJob); // { name: "John", age: 25, job: "Developer" }

// Overriding properties with spread
let updatedPerson = {
    ...person,
    age: 26
};
console.log(updatedPerson); // { name: "John", age: 26 }

// Using with function arguments
function sum(a, b, c) {
    return a + b + c;
}
let numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6
Enhanced Object Literals

ES6 introduced several enhancements to object literals.


// Property shorthand
let name = "John";
let age = 25;

// Traditional way
let person = {
    name: name,
    age: age,
    greet: function() {
        return `Hello, I'm ${this.name}`;
    }
};

// Enhanced object literal
let enhancedPerson = {
    name, // Property shorthand
    age,  // Property shorthand
    greet() { // Method shorthand
        return `Hello, I'm ${this.name}`;
    }
};

// Computed property names
let prop = "name";
let computedPerson = {
    [prop]: "John", // Computed property name
    ["age"]: 25,    // Computed property name
    ["greet" + "ing"]() { // Computed method name
        return `Hello, I'm ${this.name}`;
    }
};

console.log(computedPerson.name); // "John"
console.log(computedPerson.greeting()); // "Hello, I'm John"
Classes

ES6 introduced class syntax for creating objects, which is syntactic sugar over JavaScript's prototype-based inheritance.


// Class declaration
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    // Instance method
    greet() {
        return `Hello, I'm ${this.name}`;
    }
    
    // Getter
    get info() {
        return `${this.name} is ${this.age} years old`;
    }
    
    // Setter
    set age(newAge) {
        if (newAge > 0) {
            this._age = newAge;
        }
    }
    
    get age() {
        return this._age;
    }
    
    // Static method
    static species() {
        return "Homo sapiens";
    }
}

// Inheritance
class Student extends Person {
    constructor(name, age, grade) {
        super(name, age); // Call parent constructor
        this.grade = grade;
    }
    
    // Override method
    greet() {
        return `${super.greet()} and I'm a student in grade ${this.grade}`;
    }
    
    // New method
    study() {
        return `${this.name} is studying`;
    }
}

// Creating instances
let person = new Person("John", 25);
let student = new Student("Jane", 20, "A");

// Using instances
console.log(person.greet()); // "Hello, I'm John"
console.log(student.greet()); // "Hello, I'm Jane and I'm a student in grade A"
console.log(student.study()); // "Jane is studying"
console.log(Person.species()); // "Homo sapiens"
Modules

ES6 introduced a standardized module system for JavaScript, allowing you to export and import functionality between different files.


// math.js - Named exports
export const PI = 3.14159;
export function add(a, b) {
    return a + b;
}
export function multiply(a, b) {
    return a * b;
}

// utils.js - Default export
export default function formatDate(date) {
    return date.toLocaleDateString();
}

// main.js - Importing
import { PI, add, multiply } from './math.js';
import formatDate from './utils.js';

// Using imports
console.log(PI);
console.log(add(5, 3));
console.log(formatDate(new Date()));

// Import all as namespace
import * as math from './math.js';
console.log(math.multiply(4, 5));

// Dynamic import
async function loadModule() {
    const module = await import('./math.js');
    console.log(module.add(2, 3));
}

// Re-exporting
export { add, multiply } from './math.js';
export { default as formatDate } from './utils.js';
Promises and Async/Await

ES6 introduced Promises for handling asynchronous operations, and ES2017 introduced async/await syntax for working with Promises.


// Creating a Promise
const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Success!");
        // reject("Error!");
    }, 1000);
});

// Using a Promise
promise
    .then(data => console.log(data))
    .catch(error => console.error(error))
    .finally(() => console.log("Cleanup"));

// Promise methods
Promise.all([promise1, promise2])
    .then(results => console.log(results));

Promise.race([promise1, promise2])
    .then(result => console.log(result));

// Async/Await
async function fetchData() {
    try {
        let data = await promise;
        console.log(data);
        return data;
    } catch (error) {
        console.error(error);
    }
}

// Parallel async operations
async function fetchMultiple() {
    let [data1, data2] = await Promise.all([
        fetchData1(),
        fetchData2()
    ]);
    return { data1, data2 };
}
Other ES6+ Features

ES6 and later versions introduced many other features:


// Symbols
const symbol = Symbol('description');
const obj = {
    [symbol]: "value"
};

// Iterators and Generators
function* generator() {
    yield 1;
    yield 2;
    yield 3;
}

const iterator = generator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

// Map and Set
const map = new Map();
map.set('key', 'value');

const set = new Set([1, 2, 3, 3, 4]); // {1, 2, 3, 4}

// Optional chaining (ES2020)
const user = { profile: { name: "John" } };
const userName = user?.profile?.name; // "John"
const city = user?.profile?.address?.city; // undefined

// Nullish coalescing (ES2020)
const value = null ?? "default"; // "default"
const value2 = 0 ?? "default"; // 0

// Private class fields (ES2022)
class BankAccount {
    #balance = 0; // Private field
    
    deposit(amount) {
        this.#balance += amount;
    }
    
    getBalance() {
        return this.#balance;
    }
}
Interactive Example

Try ES6+ features:

Module 9: DOM Manipulation
Introduction to the DOM

The Document Object Model (DOM) represents the HTML document as a tree structure of nodes. JavaScript can manipulate the DOM to change content, structure, and style of web pages dynamically.

The DOM provides an API for navigating and manipulating documents. Each element in an HTML document is a node in the DOM tree, and JavaScript can access and modify these nodes.

Selecting Elements

Before you can manipulate elements, you need to select them. JavaScript provides several methods for selecting elements.

getElementById

The getElementById method returns the element with the specified ID.


// Select an element by ID
const header = document.getElementById("header");
getElementsByClassName

The getElementsByClassName method returns a collection of elements with the specified class name.


// Select elements by class name
const buttons = document.getElementsByClassName("btn");
getElementsByTagName

The getElementsByTagName method returns a collection of elements with the specified tag name.


// Select elements by tag name
const paragraphs = document.getElementsByTagName("p");
querySelector and querySelectorAll

The querySelector method returns the first element that matches a specified CSS selector. The querySelectorAll method returns all elements that match a specified CSS selector.


// Select the first element with a class
const firstButton = document.querySelector(".btn");

// Select all elements with a class
const allButtons = document.querySelectorAll(".btn");

// Select elements with complex selectors
const firstParagraphInDiv = document.querySelector("div p:first-child");
const allLinksInNavigation = document.querySelectorAll("nav a");
Creating Elements

You can create new elements using the createElement method.


// Create a new element
const newDiv = document.createElement("div");
const newParagraph = document.createElement("p");
const newImage = document.createElement("img");

// Create a text node
const newText = document.createTextNode("Hello, World!");

// Set attributes
newImage.src = "image.jpg";
newImage.alt = "Description of image";
Modifying Elements

Once you have selected or created elements, you can modify their content, attributes, and styles.

Modifying Content

// Get an element
const paragraph = document.getElementById("myParagraph");

// Change text content
paragraph.textContent = "New text content";

// Change HTML content
paragraph.innerHTML = "New HTML content";

// Append a text node
const textNode = document.createTextNode("Additional text");
paragraph.appendChild(textNode);
Modifying Attributes

// Get an element
const image = document.getElementById("myImage");

// Set attributes
image.src = "new-image.jpg";
image.alt = "New description";
image.width = "300";

// Get attributes
const source = image.src;
const alt = image.alt;

// Check if an attribute exists
if (image.hasAttribute("alt")) {
    console.log("Alt attribute exists");
}

// Remove an attribute
image.removeAttribute("width");
Modifying Styles

// Get an element
const box = document.getElementById("myBox");

// Set individual styles
box.style.color = "red";
box.style.backgroundColor = "blue";
box.style.fontSize = "16px";

// Set multiple styles using cssText
box.style.cssText = "color: red; background-color: blue; font-size: 16px;";

// Get computed styles
const computedStyle = window.getComputedStyle(box);
const color = computedStyle.color;
Modifying Classes

// Get an element
const element = document.getElementById("myElement");

// Add a class
element.classList.add("active");

// Remove a class
element.classList.remove("inactive");

// Toggle a class
element.classList.toggle("highlight");

// Check if a class exists
if (element.classList.contains("active")) {
    console.log("Element has the active class");
}

// Replace a class
element.classList.replace("old-class", "new-class");
Adding and Removing Elements

You can add elements to the DOM or remove elements from the DOM.

Adding Elements

// Create a new element
const newDiv = document.createElement("div");
newDiv.textContent = "New div element";

// Append to the body
document.body.appendChild(newDiv);

// Append to a specific element
const container = document.getElementById("container");
container.appendChild(newDiv);

// Insert before an element
const referenceElement = document.getElementById("reference");
container.insertBefore(newDiv, referenceElement);

// Insert after an element (using insertBefore with nextSibling)
container.insertBefore(newDiv, referenceElement.nextSibling);

// Prepend to an element (insert at the beginning)
container.insertBefore(newDiv, container.firstChild);
Removing Elements

// Get an element
const element = document.getElementById("myElement");

// Remove an element (modern way)
element.remove();

// Remove an element (traditional way)
const parent = element.parentNode;
parent.removeChild(element);

// Remove all child elements
const container = document.getElementById("container");
while (container.firstChild) {
    container.removeChild(container.firstChild);
}
Replacing Elements

// Create a new element
const newElement = document.createElement("div");
newElement.textContent = "New element";

// Get the element to replace
const oldElement = document.getElementById("oldElement");

// Replace the element
const parent = oldElement.parentNode;
parent.replaceChild(newElement, oldElement);
Traversing the DOM

You can navigate the DOM tree using various properties.


// Get an element
const element = document.getElementById("myElement");

// Get parent element
const parent = element.parentNode;

// Get child elements
const firstChild = element.firstChild;
const lastChild = element.lastChild;
const children = element.children;

// Get sibling elements
const previousSibling = element.previousSibling;
const nextSibling = element.nextSibling;
const previousElementSibling = element.previousElementSibling;
const nextElementSibling = element.nextElementSibling;

// Note: firstChild, lastChild, previousSibling, and nextSibling include text nodes
// firstElementChild, lastElementChild, previousElementSibling, and nextElementSibling only include element nodes
DOM Events

Events are actions that happen in the browser, like clicks, key presses, or page loads. JavaScript can respond to these events using event handlers and event listeners.

Event Listeners

// Get an element
const button = document.getElementById("myButton");

// Add an event listener
button.addEventListener("click", function() {
    console.log("Button clicked!");
});

// Add an event listener with an arrow function
button.addEventListener("click", () => {
    console.log("Button clicked with arrow function!");
});

// Add an event listener with a named function
function handleClick() {
    console.log("Button clicked with named function!");
}
button.addEventListener("click", handleClick);

// Remove an event listener
button.removeEventListener("click", handleClick);
Event Object

When an event occurs, an event object is created with information about the event.


// Add an event listener with event object
button.addEventListener("click", function(event) {
    console.log("Event type:", event.type);
    console.log("Target element:", event.target);
    console.log("Current target:", event.currentTarget);
    
    // Prevent default behavior
    event.preventDefault();
    
    // Stop event propagation
    event.stopPropagation();
});
Event Delegation

Event delegation is a technique where you add a single event listener to a parent element to handle events for multiple child elements.


// Add a single event listener to a parent element
document.addEventListener("click", function(event) {
    // Check if the clicked element matches a specific selector
    if (event.target.matches(".button")) {
        console.log("Delegated button click");
    }
});
Interactive Example

Try DOM manipulation:

This is a demo area for DOM manipulation.

Module 10: Event Handling
Introduction to Events

Events are actions that happen in the browser, such as a user clicking a button, pressing a key, or the page finishing loading. JavaScript can respond to these events using event handlers and event listeners.

Understanding events is crucial for creating interactive web applications. Events allow you to respond to user actions and make your web pages dynamic and responsive.

Event Listeners

Event listeners are functions that wait for a specific event to occur on a specific element. When the event occurs, the listener function is executed.

addEventListener Method

The addEventListener method attaches an event handler to an element without overwriting existing event handlers.


// Get an element
const button = document.getElementById("myButton");

// Add an event listener
button.addEventListener("click", function() {
    console.log("Button clicked!");
});

// Add multiple event listeners to the same element
button.addEventListener("click", function() {
    console.log("Another click handler!");
});

// Add an event listener with an arrow function
button.addEventListener("click", () => {
    console.log("Arrow function click handler!");
});

// Add an event listener with a named function
function handleClick() {
    console.log("Named function click handler!");
}
button.addEventListener("click", handleClick);
Removing Event Listeners

You can remove event listeners using the removeEventListener method. To remove an event listener, you need a reference to the same function that was added.


// Add an event listener with a named function
function handleClick() {
    console.log("Button clicked!");
}
button.addEventListener("click", handleClick);

// Remove the event listener
button.removeEventListener("click", handleClick);

// You cannot remove anonymous event listeners
button.addEventListener("click", function() {
    console.log("This cannot be removed!");
});
// This won't work because the function is anonymous
button.removeEventListener("click", function() {
    console.log("This cannot be removed!");
});
Event Options

The addEventListener method can take an options object as a third argument.


// Add an event listener with options
button.addEventListener("click", function() {
    console.log("Button clicked!");
}, {
    capture: false,    // Whether to use the capturing phase
    once: true,       // Whether the listener should be removed after being called once
    passive: false    // Whether the listener will call preventDefault()
});

// Shorthand for once option
button.addEventListener("click", function() {
    console.log("This will only be called once!");
}, { once: true });
Event Object

When an event occurs, an event object is created with information about the event. This object is passed as an argument to the event handler function.

Common Event Properties

// Add an event listener with event object
button.addEventListener("click", function(event) {
    console.log("Event type:", event.type);        // "click"
    console.log("Target element:", event.target);  // The element that triggered the event
    console.log("Current target:", event.currentTarget); // The element the listener is attached to
    console.log("Timestamp:", event.timeStamp);    // When the event occurred
    console.log("Coordinates:", event.clientX, event.clientY); // Mouse position
});

// Keyboard events
document.addEventListener("keydown", function(event) {
    console.log("Key pressed:", event.key);        // The key that was pressed
    console.log("Key code:", event.keyCode);        // The key code
    console.log("Shift key:", event.shiftKey);      // Whether shift was pressed
    console.log("Ctrl key:", event.ctrlKey);        // Whether ctrl was pressed
    console.log("Alt key:", event.altKey);          // Whether alt was pressed
});
Event Methods

// Prevent default behavior
document.querySelector("a").addEventListener("click", function(event) {
    event.preventDefault(); // Prevents the link from being followed
});

// Stop event propagation
document.querySelector("div").addEventListener("click", function(event) {
    event.stopPropagation(); // Prevents the event from bubbling up to parent elements
});

// Prevent default and stop propagation
form.addEventListener("submit", function(event) {
    event.preventDefault(); // Prevents the form from being submitted
    event.stopPropagation(); // Prevents the event from bubbling up
});
Event Phases

Events propagate through the DOM in three phases: capturing phase, target phase, and bubbling phase.

Capturing Phase

In the capturing phase, the event travels from the window down to the target element.


// Add an event listener for the capturing phase
document.addEventListener("click", function(event) {
    console.log("Document capturing phase");
}, { capture: true });

document.querySelector("div").addEventListener("click", function(event) {
    console.log("Div capturing phase");
}, { capture: true });

document.querySelector("button").addEventListener("click", function(event) {
    console.log("Button target phase");
}, { capture: true });
Bubbling Phase

In the bubbling phase, the event travels from the target element up to the window.


// Add an event listener for the bubbling phase (default)
document.addEventListener("click", function(event) {
    console.log("Document bubbling phase");
});

document.querySelector("div").addEventListener("click", function(event) {
    console.log("Div bubbling phase");
});

document.querySelector("button").addEventListener("click", function(event) {
    console.log("Button target phase");
});
Common Event Types

JavaScript supports many different types of events. Here are some of the most common ones:

Mouse Events

// Click - when the element is clicked
element.addEventListener("click", function(event) {
    console.log("Element clicked");
});

// Double click - when the element is double-clicked
element.addEventListener("dblclick", function(event) {
    console.log("Element double-clicked");
});

// Mouse down - when a mouse button is pressed down
element.addEventListener("mousedown", function(event) {
    console.log("Mouse button pressed");
});

// Mouse up - when a mouse button is released
element.addEventListener("mouseup", function(event) {
    console.log("Mouse button released");
});

// Mouse over - when the mouse pointer enters the element
element.addEventListener("mouseover", function(event) {
    console.log("Mouse entered element");
});

// Mouse out - when the mouse pointer leaves the element
element.addEventListener("mouseout", function(event) {
    console.log("Mouse left element");
});

// Mouse move - when the mouse pointer moves over the element
element.addEventListener("mousemove", function(event) {
    console.log("Mouse moved over element");
});
Keyboard Events

// Key down - when a key is pressed down
document.addEventListener("keydown", function(event) {
    console.log("Key pressed:", event.key);
});

// Key up - when a key is released
document.addEventListener("keyup", function(event) {
    console.log("Key released:", event.key);
});

// Key press - when a key that produces a character value is pressed
document.addEventListener("keypress", function(event) {
    console.log("Character key pressed:", event.key);
});
Form Events

// Submit - when a form is submitted
form.addEventListener("submit", function(event) {
    event.preventDefault(); // Prevent the form from submitting
    console.log("Form submitted");
});

// Change - when the value of an element changes
input.addEventListener("change", function(event) {
    console.log("Input value changed:", event.target.value);
});

// Input - when the value of an element changes (for every keystroke)
input.addEventListener("input", function(event) {
    console.log("Input value:", event.target.value);
});

// Focus - when an element receives focus
input.addEventListener("focus", function(event) {
    console.log("Input received focus");
});

// Blur - when an element loses focus
input.addEventListener("blur", function(event) {
    console.log("Input lost focus");
});
Document/Window Events

// Load - when the page is fully loaded
window.addEventListener("load", function(event) {
    console.log("Page fully loaded");
});

// DOMContentLoaded - when the DOM is fully loaded
document.addEventListener("DOMContentLoaded", function(event) {
    console.log("DOM fully loaded");
});

// Resize - when the browser window is resized
window.addEventListener("resize", function(event) {
    console.log("Window resized");
});

// Scroll - when the document is scrolled
window.addEventListener("scroll", function(event) {
    console.log("Document scrolled");
});
Event Delegation

Event delegation is a technique where you add a single event listener to a parent element to handle events for multiple child elements. This is more efficient than adding a separate event listener to each child element.


// Add a single event listener to a parent element
document.addEventListener("click", function(event) {
    // Check if the clicked element matches a specific selector
    if (event.target.matches(".button")) {
        console.log("Delegated button click");
    }
    
    // Check if the clicked element is within a specific element
    if (event.target.closest(".card")) {
        console.log("Clicked within a card");
    }
});

// Example with a list
document.querySelector("ul").addEventListener("click", function(event) {
    if (event.target.tagName === "LI") {
        console.log("List item clicked:", event.target.textContent);
    }
});
Custom Events

You can create and dispatch your own custom events.


// Create a custom event
const customEvent = new Event("customEvent");

// Create a custom event with details
const detailedEvent = new CustomEvent("detailedEvent", {
    detail: { message: "This is a custom event with details" }
});

// Add an event listener for the custom event
document.addEventListener("customEvent", function(event) {
    console.log("Custom event fired");
});

document.addEventListener("detailedEvent", function(event) {
    console.log("Detailed event fired:", event.detail.message);
});

// Dispatch the custom event
document.dispatchEvent(customEvent);
document.dispatchEvent(detailedEvent);
Interactive Example

Try different event types:

Event output will appear here...
Module 11: Asynchronous JavaScript
Introduction to Asynchronous JavaScript

JavaScript is single-threaded, meaning it can only do one thing at a time. However, it can handle asynchronous operations using callbacks, promises, and async/await. This is essential for operations that take time, like API calls, file operations, or timers.

Understanding the event loop, callback queue, and microtask queue is crucial for mastering asynchronous JavaScript.

The Event Loop

The event loop is a mechanism that allows JavaScript to perform non-blocking operations despite being single-threaded. It continuously checks the call stack and the task queue, and pushes tasks from the queue to the stack when the stack is empty.


console.log("Start");

setTimeout(function() {
    console.log("Timeout");
}, 0);

Promise.resolve().then(function() {
    console.log("Promise");
});

console.log("End");

// Output:
// Start
// End
// Promise
// Timeout

// Explanation:
// 1. "Start" is logged to the console.
// 2. setTimeout is added to the task queue.
// 3. Promise.resolve().then() is added to the microtask queue.
// 4. "End" is logged to the console.
// 5. The call stack is empty, so the event loop checks the microtask queue first.
// 6. The promise callback is executed, logging "Promise".
// 7. The event loop checks the task queue.
// 8. The timeout callback is executed, logging "Timeout".
Callbacks

Callbacks are functions that are passed as arguments to other functions and are executed after an operation completes. Callbacks were the original way to handle asynchronous operations in JavaScript.

Simple Callback Example

function fetchData(callback) {
    setTimeout(function() {
        const data = "Some data";
        callback(data);
    }, 1000);
}

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

fetchData(processData);
// After 1 second: "Processing data: Some data"
Callback Hell

Nesting multiple callbacks can lead to "callback hell" or "pyramid of doom," which makes the code hard to read and maintain.


// Callback hell example
fetchData(function(data1) {
    processData1(data1, function(result1) {
        fetchData2(function(data2) {
            processData2(data2, function(result2) {
                fetchData3(function(data3) {
                    processData3(data3, function(result3) {
                        console.log("Final result:", result3);
                    });
                });
            });
        });
    });
});
Promises

Promises provide a cleaner way to handle asynchronous operations. A promise represents a value that may be available now, or in the future, or never.

Creating a Promise

const promise = new Promise(function(resolve, reject) {
    // Asynchronous operation
    setTimeout(function() {
        const success = true;
        
        if (success) {
            resolve("Success!"); // Resolve the promise with a value
        } else {
            reject("Error!"); // Reject the promise with a reason
        }
    }, 1000);
});

// Using the promise
promise
    .then(function(data) {
        console.log(data); // "Success!"
    })
    .catch(function(error) {
        console.error(error); // This won't be executed in this example
    });
Promise Methods

// Promise.all - waits for all promises to resolve
const promise1 = Promise.resolve(3);
const promise2 = new Promise(function(resolve) {
    setTimeout(resolve, 100, "foo");
});
const promise3 = Promise.resolve(42);

Promise.all([promise1, promise2, promise3])
    .then(function(values) {
        console.log(values); // [3, "foo", 42]
    })
    .catch(function(error) {
        console.error(error);
    });

// Promise.race - waits for the first promise to resolve or reject
const promise4 = new Promise(function(resolve) {
    setTimeout(resolve, 500, "one");
});
const promise5 = new Promise(function(resolve) {
    setTimeout(resolve, 100, "two");
});

Promise.race([promise4, promise5])
    .then(function(value) {
        console.log(value); // "two"
    })
    .catch(function(error) {
        console.error(error);
    });

// Promise.allSettled - waits for all promises to settle (resolve or reject)
const promise6 = Promise.resolve(3);
const promise7 = new Promise(function(resolve, reject) {
    setTimeout(reject, 100, "error");
});

Promise.allSettled([promise6, promise7])
    .then(function(results) {
        console.log(results);
        // [
        //   { status: "fulfilled", value: 3 },
        //   { status: "rejected", reason: "error" }
        // ]
    });

// Promise.any - waits for the first promise to fulfill
const promise8 = Promise.reject("error1");
const promise9 = Promise.reject("error2");
const promise10 = Promise.resolve("success");

Promise.any([promise8, promise9, promise10])
    .then(function(value) {
        console.log(value); // "success"
    })
    .catch(function(error) {
        console.error(error); // AggregateError: All promises were rejected
    });
Chaining Promises

function fetchData() {
    return new Promise(function(resolve) {
        setTimeout(function() {
            resolve("Data fetched");
        }, 1000);
    });
}

function processData(data) {
    return new Promise(function(resolve) {
        setTimeout(function() {
            resolve(data + " and processed");
        }, 1000);
    });
}

function saveData(data) {
    return new Promise(function(resolve) {
        setTimeout(function() {
            resolve(data + " and saved");
        }, 1000);
    });
}

// Chaining promises
fetchData()
    .then(function(data) {
        console.log(data); // "Data fetched"
        return processData(data);
    })
    .then(function(data) {
        console.log(data); // "Data fetched and processed"
        return saveData(data);
    })
    .then(function(data) {
        console.log(data); // "Data fetched and processed and saved"
    })
    .catch(function(error) {
        console.error(error);
    });
Async/Await

Async/await is syntactic sugar over promises that makes asynchronous code look and behave more like synchronous code. It was introduced in ES2017.

Async Functions

// Declaring an async function
async function fetchData() {
    return "Data fetched";
}

// Calling an async function
fetchData().then(function(data) {
    console.log(data); // "Data fetched"
});

// Using await inside an async function
async function processData() {
    const data = await fetchData(); // Waits for the promise to resolve
    return data + " and processed";
}

processData().then(function(data) {
    console.log(data); // "Data fetched and processed"
});
Error Handling with Async/Await

async function fetchData() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject("Error fetching data");
        }, 1000);
    });
}

// Using try/catch for error handling
async function processData() {
    try {
        const data = await fetchData();
        return data + " and processed";
    } catch (error) {
        console.error(error); // "Error fetching data"
        return "Default data";
    }
}

processData().then(function(data) {
    console.log(data); // "Default data"
});
Parallel Async Operations

async function fetchMultipleData() {
    // Fetch data in parallel
    const [data1, data2, data3] = await Promise.all([
        fetchData1(),
        fetchData2(),
        fetchData3()
    ]);
    
    return { data1, data2, data3 };
}

// Sequential async operations
async function fetchSequentialData() {
    const data1 = await fetchData1();
    const data2 = await fetchData2();
    const data3 = await fetchData3();
    
    return { data1, data2, data3 };
}
Fetch API

The Fetch API provides a modern interface for fetching resources. It is more powerful and flexible than the older XMLHttpRequest.

Basic Fetch

// Basic fetch with promises
fetch("https://api.example.com/data")
    .then(function(response) {
        if (!response.ok) {
            throw new Error("Network response was not ok");
        }
        return response.json();
    })
    .then(function(data) {
        console.log(data);
    })
    .catch(function(error) {
        console.error("Fetch error:", error);
    });

// Basic fetch with async/await
async function fetchData() {
    try {
        const response = await fetch("https://api.example.com/data");
        
        if (!response.ok) {
            throw new Error("Network response was not ok");
        }
        
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error("Fetch error:", error);
    }
}
Fetch Options

// POST request with fetch
async function postData(url, data) {
    try {
        const response = await fetch(url, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(data),
        });
        
        if (!response.ok) {
            throw new Error("Network response was not ok");
        }
        
        const responseData = await response.json();
        return responseData;
    } catch (error) {
        console.error("Fetch error:", error);
    }
}

// Using the postData function
postData("https://api.example.com/data", { name: "John", age: 25 })
    .then(function(data) {
        console.log(data);
    });
Interactive Example

Try asynchronous operations:

Async output will appear here...
Module 12: Error Handling and Debugging
Introduction to Error Handling

Errors are inevitable in programming. JavaScript provides mechanisms for handling errors gracefully, making your code more robust and user-friendly. Proper error handling prevents your application from crashing and provides meaningful feedback to users.

Types of Errors

JavaScript has several built-in error types, each representing a different kind of error:

  • Error: The base error type, all other error types inherit from it
  • SyntaxError: Occurs when there's a syntax error in the code
  • ReferenceError: Occurs when trying to access a non-existent variable
  • TypeError: Occurs when an operation is performed on a value of the wrong type
  • RangeError: Occurs when a numeric value is outside its allowed range
  • URIError: Occurs when using encodeURI() or decodeURI() with invalid URIs
  • EvalError: Occurs when using the eval() function (rarely used now)
Try-Catch-Finally

The try-catch-finally statement is used to handle errors. The try block contains the code that might throw an error, the catch block contains the code to handle the error, and the finally block contains code that will always execute, regardless of whether an error occurred.


try {
    // Code that might throw an error
    let result = riskyOperation();
    console.log(result);
} catch (error) {
    // Code to handle the error
    console.error("Error occurred:", error.message);
} finally {
    // Code that will always execute
    console.log("Cleanup code");
}

// Example with a specific error
try {
    let obj = null;
    console.log(obj.property); // Throws a TypeError
} catch (error) {
    if (error instanceof TypeError) {
        console.error("Type error occurred:", error.message);
    } else {
        console.error("Unknown error:", error);
    }
}
Throwing Errors

You can throw your own errors using the throw statement. This is useful for validating input or handling specific conditions in your code.


function divide(a, b) {
    if (b === 0) {
        throw new Error("Division by zero is not allowed");
    }
    return a / b;
}

try {
    let result = divide(10, 0);
    console.log(result);
} catch (error) {
    console.error(error.message); // "Division by zero is not allowed"
}

// Throwing different types of errors
function validateAge(age) {
    if (typeof age !== "number") {
        throw new TypeError("Age must be a number");
    }
    if (age < 0) {
        throw new RangeError("Age cannot be negative");
    }
    if (age > 120) {
        throw new RangeError("Age seems unrealistic");
    }
    return true;
}

try {
    validateAge("twenty");
} catch (error) {
    console.error(error.name + ": " + error.message); // "TypeError: Age must be a number"
}
Custom Error Types

You can create your own error types by extending the Error class. This allows you to handle specific errors in your application.


// Custom error type
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

// Another custom error type
class NetworkError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.name = "NetworkError";
        this.statusCode = statusCode;
    }
}

// Using custom errors
function validateEmail(email) {
    if (!email.includes("@")) {
        throw new ValidationError("Invalid email format");
    }
    return true;
}

function fetchData(url) {
    return new Promise((resolve, reject) => {
        // Simulate a network request
        setTimeout(() => {
            if (url.startsWith("https://")) {
                resolve("Data fetched successfully");
            } else {
                reject(new NetworkError("Insecure protocol", 400));
            }
        }, 1000);
    });
}

// Handling custom errors
try {
    validateEmail("invalid-email");
} catch (error) {
    if (error instanceof ValidationError) {
        console.error("Validation error:", error.message);
    } else {
        console.error("Unknown error:", error);
    }
}

fetchData("http://example.com")
    .then(data => console.log(data))
    .catch(error => {
        if (error instanceof NetworkError) {
            console.error(`Network error (${error.statusCode}): ${error.message}`);
        } else {
            console.error("Unknown error:", error);
        }
    });
Global Error Handlers

You can set up global error handlers to catch errors that aren't caught by try-catch blocks.


// Global error handler for synchronous errors
window.addEventListener("error", function(event) {
    console.error("Global error:", event.error);
    // You might want to send the error to a logging service
});

// Global error handler for unhandled promise rejections
window.addEventListener("unhandledrejection", function(event) {
    console.error("Unhandled promise rejection:", event.reason);
    // You might want to send the error to a logging service
    event.preventDefault(); // Prevents the default browser behavior
});

// Using these global handlers is especially useful in production
// to catch and log unexpected errors
Debugging Techniques

Debugging is the process of finding and fixing errors in your code. JavaScript provides several tools and techniques for debugging.

Console Methods

// console.log - for general output
console.log("Variable value:", variable);

// console.error - for errors
console.error("Something went wrong:", error);

// console.warn - for warnings
console.warn("This might cause issues");

// console.info - for informational messages
console.info("Process completed successfully");

// console.table - for displaying tabular data
const users = [
    { id: 1, name: "John", age: 25 },
    { id: 2, name: "Jane", age: 30 }
];
console.table(users);

// console.group and console.groupEnd - for grouping related messages
console.group("User validation");
console.log("Validating user data");
console.log("User data is valid");
console.groupEnd();

// console.trace - for showing the call stack
function functionA() {
    functionB();
}

function functionB() {
    console.trace("Trace from functionB");
}

functionA();

// console.time and console.timeEnd - for measuring execution time
console.time("Array processing");
// Some array processing code
console.timeEnd("Array processing");
Debugger Statement

function calculateTotal(items) {
    debugger; // Execution will pause here if developer tools are open
    
    let total = 0;
    for (let i = 0; i < items.length; i++) {
        total += items[i].price;
    }
    
    return total;
}

// The debugger statement is useful when you want to pause execution
// at a specific point without setting a breakpoint manually
Using Browser Developer Tools

Modern browsers provide powerful developer tools for debugging JavaScript:

  • Console: For viewing logs and running JavaScript code
  • Sources: For viewing and debugging source code
  • Network: For monitoring network requests
  • Performance: For analyzing performance
Error Handling Best Practices

Follow these best practices for effective error handling:

  • Be Specific: Catch specific errors rather than using a generic catch-all
  • Log Errors: Log errors to help with debugging
  • Provide Feedback: Give users meaningful feedback when errors occur
  • Fail Gracefully: Ensure your application doesn't crash when errors occur
  • Validate Input: Validate input to prevent errors before they occur

// Example of good error handling
async function fetchUserData(userId) {
    try {
        // Validate input
        if (!userId || typeof userId !== "string") {
            throw new ValidationError("Invalid user ID");
        }
        
        // Fetch data
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
            throw new NetworkError(`Failed to fetch user data: ${response.statusText}`, response.status);
        }
        
        const userData = await response.json();
        
        // Validate response data
        if (!userData || !userData.id) {
            throw new ValidationError("Invalid user data received");
        }
        
        return userData;
    } catch (error) {
        // Log the error for debugging
        console.error("Error fetching user data:", error);
        
        // Provide meaningful feedback to the user
        if (error instanceof ValidationError) {
            showUserMessage("Invalid input. Please check your user ID and try again.");
        } else if (error instanceof NetworkError) {
            showUserMessage("Network error. Please check your connection and try again.");
        } else {
            showUserMessage("An unexpected error occurred. Please try again later.");
        }
        
        // Re-throw the error if you want calling code to handle it
        throw error;
    }
}

// Using the function with proper error handling
try {
    const userData = await fetchUserData("user123");
    displayUserData(userData);
} catch (error) {
    // Error is already handled in the function, but we might want to do additional cleanup
    hideLoadingIndicator();
}
Interactive Example

Try error handling:

Error handling output will appear here...
Module 13: Classes and OOP
Introduction to Object-Oriented Programming

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects," which can contain data and code. JavaScript supports OOP through prototypes, and ES6 introduced class syntax that makes OOP more intuitive.

OOP concepts include encapsulation, inheritance, polymorphism, and abstraction. Understanding these concepts will help you write more organized, reusable, and maintainable code.

Class Syntax

ES6 introduced class syntax for creating objects, which is syntactic sugar over JavaScript's prototype-based inheritance. Classes provide a cleaner way to create objects and implement inheritance.

Class Declaration

// Class declaration
class Person {
    // Constructor method
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    // Instance method
    greet() {
        return `Hello, I'm ${this.name}`;
    }
    
    // Another instance method
    celebrateBirthday() {
        this.age++;
        return `Happy birthday! You are now ${this.age} years old.`;
    }
}

// Creating an instance of the class
const person = new Person("John", 25);
console.log(person.greet()); // "Hello, I'm John"
console.log(person.celebrateBirthday()); // "Happy birthday! You are now 26 years old."
Class Expression

// Class expression (unnamed)
const Person = class {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        return `Hello, I'm ${this.name}`;
    }
};

// Class expression (named)
const Person = class PersonClass {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        return `Hello, I'm ${this.name}`;
    }
    
    getClassName() {
        return PersonClass.name;
    }
};

const person = new Person("John", 25);
console.log(person.getClassName()); // "PersonClass"
Fields and Methods

Classes can have fields (properties) and methods (functions). Fields can be instance fields (unique to each instance) or static fields (shared by all instances).

Instance Fields

class Person {
    // Instance fields (can be declared outside constructor in ES2022)
    name;
    age;
    
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    // Instance method
    greet() {
        return `Hello, I'm ${this.name}`;
    }
}

const person1 = new Person("John", 25);
const person2 = new Person("Jane", 30);

console.log(person1.name); // "John"
console.log(person2.name); // "Jane"
Static Fields and Methods

class Person {
    // Static field
    static species = "Homo sapiens";
    
    // Static method
    static getSpecies() {
        return Person.species;
    }
    
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    // Instance method
    greet() {
        return `Hello, I'm ${this.name} and I'm a ${Person.species}`;
    }
}

// Accessing static fields and methods
console.log(Person.species); // "Homo sapiens"
console.log(Person.getSpecies()); // "Homo sapiens"

// Creating an instance
const person = new Person("John", 25);
console.log(person.greet()); // "Hello, I'm John and I'm a Homo sapiens"
Private Fields and Methods

class BankAccount {
    // Private field (ES2022)
    #balance;
    
    constructor(initialBalance) {
        this.#balance = initialBalance;
    }
    
    // Public method
    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
            return this.#balance;
        }
        throw new Error("Deposit amount must be positive");
    }
    
    // Public method
    withdraw(amount) {
        if (amount > 0 && amount <= this.#balance) {
            this.#balance -= amount;
            return this.#balance;
        }
        throw new Error("Invalid withdrawal amount");
    }
    
    // Public method
    getBalance() {
        return this.#balance;
    }
    
    // Private method (ES2022)
    #validateAmount(amount) {
        return typeof amount === "number" && amount > 0;
    }
}

const account = new BankAccount(1000);
console.log(account.getBalance()); // 1000
account.deposit(500);
console.log(account.getBalance()); // 1500

// Trying to access private field directly
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
Getters and Setters

Getters and setters allow you to define how properties are accessed and modified. They provide a way to execute code when getting or setting a property value.


class Person {
    constructor(name, age) {
        this._name = name; // Convention: underscore for "private" properties
        this._age = age;
    }
    
    // Getter for name
    get name() {
        return this._name;
    }
    
    // Setter for name
    set name(newName) {
        if (typeof newName === "string" && newName.length > 0) {
            this._name = newName;
        } else {
            throw new Error("Name must be a non-empty string");
        }
    }
    
    // Getter for age
    get age() {
        return this._age;
    }
    
    // Setter for age
    set age(newAge) {
        if (typeof newAge === "number" && newAge >= 0) {
            this._age = newAge;
        } else {
            throw new Error("Age must be a non-negative number");
        }
    }
    
    // Computed getter
    get info() {
        return `${this._name} is ${this._age} years old`;
    }
}

const person = new Person("John", 25);
console.log(person.name); // "John" (calls the getter)
console.log(person.age); // 25 (calls the getter)
console.log(person.info); // "John is 25 years old" (calls the computed getter)

person.name = "Jane"; // Calls the setter
person.age = 26; // Calls the setter
console.log(person.info); // "Jane is 26 years old"

// person.name = ""; // Throws an error
// person.age = -5; // Throws an error
Inheritance

Inheritance allows a class to inherit properties and methods from another class. The class that inherits is called the subclass or derived class, and the class being inherited from is called the superclass or base class.

Extending a Class

// Base class
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        return `Hello, I'm ${this.name}`;
    }
}

// Derived class
class Student extends Person {
    constructor(name, age, grade) {
        // Call the parent constructor
        super(name, age);
        this.grade = grade;
    }
    
    // Override the greet method
    greet() {
        return `${super.greet()} and I'm a student in grade ${this.grade}`;
    }
    
    // New method
    study() {
        return `${this.name} is studying hard`;
    }
}

const student = new Student("John", 15, 10);
console.log(student.greet()); // "Hello, I'm John and I'm a student in grade 10"
console.log(student.study()); // "John is studying hard"
Overriding Methods

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    speak() {
        return `${this.name} makes a sound`;
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
    
    // Override the speak method
    speak() {
        return `${this.name} barks`;
    }
    
    // Call the parent method
    speakLikeAnimal() {
        return super.speak();
    }
}

const dog = new Dog("Rex", "Labrador");
console.log(dog.speak()); // "Rex barks"
console.log(dog.speakLikeAnimal()); // "Rex makes a sound"
Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common superclass. This is achieved through method overriding and interfaces.


class Shape {
    calculateArea() {
        throw new Error("Method must be implemented by subclass");
    }
}

class Rectangle extends Shape {
    constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
    }
    
    calculateArea() {
        return this.width * this.height;
    }
}

class Circle extends Shape {
    constructor(radius) {
        super();
        this.radius = radius;
    }
    
    calculateArea() {
        return Math.PI * this.radius * this.radius;
    }
}

// Polymorphism in action
function printArea(shape) {
    console.log(`Area: ${shape.calculateArea()}`);
}

const rectangle = new Rectangle(5, 10);
const circle = new Circle(7);

printArea(rectangle); // "Area: 50"
printArea(circle); // "Area: 153.93804002589985"
Abstract Classes

JavaScript doesn't have built-in support for abstract classes, but you can simulate them using regular classes and throwing errors in methods that should be implemented by subclasses.


// Simulating an abstract class
class Animal {
    constructor(name) {
        if (this.constructor === Animal) {
            throw new Error("Animal is an abstract class and cannot be instantiated directly");
        }
        this.name = name;
    }
    
    // Abstract method
    speak() {
        throw new Error("Method 'speak' must be implemented by subclass");
    }
    
    // Concrete method
    eat() {
        return `${this.name} is eating`;
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
    
    // Implement the abstract method
    speak() {
        return `${this.name} barks`;
    }
}

// const animal = new Animal("Generic Animal"); // Throws an error
const dog = new Dog("Rex", "Labrador");
console.log(dog.speak()); // "Rex barks"
console.log(dog.eat()); // "Rex is eating"
Mixins

Mixins are a way to reuse code in multiple class hierarchies. They are classes that provide methods that can be used by other classes.


// Mixin class
const CanFly = {
    fly() {
        return `${this.name} is flying`;
    }
};

const CanSwim = {
    swim() {
        return `${this.name} is swimming`;
    }
};

// Using mixins with classes
class Bird {
    constructor(name) {
        this.name = name;
    }
}

// Apply the mixin
Object.assign(Bird.prototype, CanFly);

const bird = new Bird("Tweety");
console.log(bird.fly()); // "Tweety is flying"

// Using multiple mixins
class Duck {
    constructor(name) {
        this.name = name;
    }
}

// Apply multiple mixins
Object.assign(Duck.prototype, CanFly, CanSwim);

const duck = new Duck("Donald");
console.log(duck.fly()); // "Donald is flying"
console.log(duck.swim()); // "Donald is swimming"
Interactive Example

Try creating and using classes:

Class output will appear here...
Module 14: Modules and Imports
Introduction to Modules

JavaScript modules allow you to break up your code into separate, reusable files. ES6 introduced a standardized module system with import and export syntax. Modules help organize code, avoid namespace pollution, and enable better code reuse.

Before ES6 modules, JavaScript relied on various module systems like CommonJS (used in Node.js) and AMD (Asynchronous Module Definition). ES6 modules provide a native solution that works in both browsers and Node.js.

Exporting from Modules

You can export functions, objects, or primitive values from a module using the export keyword. There are two types of exports: named exports and default exports.

Named Exports

// math.js

// Exporting a variable
export const PI = 3.14159;

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

// Exporting another function
export function multiply(a, b) {
    return a * b;
}

// Exporting a class
export class Calculator {
    constructor() {
        this.result = 0;
    }
    
    add(value) {
        this.result += value;
        return this;
    }
    
    subtract(value) {
        this.result -= value;
        return this;
    }
    
    getResult() {
        return this.result;
    }
}

// Exporting an object
export const utils = {
    formatNumber: (num) => num.toFixed(2),
    isEven: (num) => num % 2 === 0
};
Default Exports

// utils.js

// Default export of a function
export default function formatDate(date) {
    return date.toLocaleDateString();
}

// You can also have named exports alongside a default export
export const formatTime = (date) => {
    return date.toLocaleTimeString();
};

// Default export of a class
// export default class DateFormatter {
//     format(date) {
//         return date.toLocaleDateString();
//     }
// }
Exporting After Declaration

// helpers.js

const API_URL = "https://api.example.com";

function fetchData(endpoint) {
    return fetch(`${API_URL}/${endpoint}`);
}

class ApiClient {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
    }
    
    async get(endpoint) {
        const response = await fetch(`${this.baseUrl}/${endpoint}`);
        return response.json();
    }
}

// Exporting after declaration
export { API_URL, fetchData, ApiClient };
Re-exporting

// index.js

// Re-exporting named exports
export { add, multiply, PI } from './math.js';

// Re-exporting with different names
export { add as sum, multiply as product } from './math.js';

// Re-exporting a default export
export { default as formatDate } from './utils.js';

// Re-exporting all from a module
export * from './helpers.js';

// Re-exporting all named exports from a module
export * as MathUtils from './math.js';
Importing Modules

You can import exported values from other modules using the import keyword. The import statement must be at the top level of a module.

Importing Named Exports

// main.js

// Import specific named exports
import { PI, add, multiply } from './math.js';

// Using the imports
console.log(PI); // 3.14159
console.log(add(5, 3)); // 8
console.log(multiply(5, 3)); // 15

// Import with different names
import { add as sum, multiply as product } from './math.js';

console.log(sum(5, 3)); // 8
console.log(product(5, 3)); // 15

// Import all named exports as a namespace
import * as MathUtils from './math.js';

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.multiply(5, 3)); // 15
Importing Default Exports

// main.js

// Import a default export
import formatDate from './utils.js';

// Using the import
const today = new Date();
console.log(formatDate(today)); // "5/15/2023" (format depends on locale)

// Import a default export with a different name
import dateFormatter from './utils.js';

console.log(dateFormatter(today)); // "5/15/2023"

// Import both default and named exports
import formatDate, { formatTime } from './utils.js';

console.log(formatDate(today)); // "5/15/2023"
console.log(formatTime(today)); // "2:30:45 PM" (format depends on locale)
Importing All Exports

// main.js

// Import all exports from a module
import * as Utils from './utils.js';

// Using the imports
const today = new Date();
console.log(Utils.formatDate(today)); // "5/15/2023"
console.log(Utils.formatTime(today)); // "2:30:45 PM"

// Import all exports from a module with a namespace
import * as MathUtils from './math.js';

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(5, 3)); // 8
Dynamic Imports

Dynamic imports allow you to load modules on demand, which can improve performance by only loading code when it's needed. Dynamic imports return a promise.


// main.js

// Dynamic import of a module
async function loadMathModule() {
    try {
        const MathUtils = await import('./math.js');
        console.log(MathUtils.add(5, 3)); // 8
        console.log(MathUtils.multiply(5, 3)); // 15
    } catch (error) {
        console.error("Failed to load math module:", error);
    }
}

// Call the function to load the module
loadMathModule();

// Using dynamic imports with conditional loading
async function loadFeature(feature) {
    if (feature === "math") {
        const MathUtils = await import('./math.js');
        return MathUtils;
    } else if (feature === "utils") {
        const Utils = await import('./utils.js');
        return Utils;
    }
}

// Using the function
loadFeature("math").then(MathUtils => {
    console.log(MathUtils.add(5, 3)); // 8
});
Module Loading in HTML

To use ES6 modules in an HTML file, you need to set the type attribute of the script tag to "module".


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ES6 Modules Example</title>
</head>
<body>
    <h1>ES6 Modules Example</h1>
    <div id="output"></div>
    
    <!-- Load the main module -->
    <script type="module" src="main.js"></script>
</body>
</html>
Module Bundlers

Module bundlers like Webpack, Rollup, and Parcel are tools that take modules with dependencies and bundle them into static assets that browsers can understand. Bundlers are commonly used in modern web development to optimize the loading and execution of JavaScript code.

CommonJS vs. ES6 Modules

CommonJS is the module system used by Node.js, while ES6 modules are the standard for JavaScript. Here are the key differences:

  • Syntax: CommonJS uses require() and module.exports, while ES6 modules use import and export
  • Loading: CommonJS modules are loaded synchronously, while ES6 modules are loaded asynchronously
  • this: In CommonJS, this refers to the module exports, while in ES6 modules, this is undefined
  • Imports: CommonJS imports are copies of the exported values, while ES6 module imports are live bindings to the exported values

// CommonJS (Node.js)
// math.js
const PI = 3.14159;

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

module.exports = { PI, add };

// main.js
const { PI, add } = require('./math.js');
console.log(PI); // 3.14159
console.log(add(5, 3)); // 8

// ES6 Modules
// math.js
export const PI = 3.14159;

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

// main.js
import { PI, add } from './math.js';
console.log(PI); // 3.14159
console.log(add(5, 3)); // 8
Interactive Example

Try module concepts:

Module output will appear here...
Module 15: Data Structures
Introduction to Data Structures

Data structures are ways of organizing and storing data so that it can be accessed and modified efficiently. Different data structures are suited for different kinds of applications, and some are highly specialized to specific tasks.

Understanding data structures is crucial for writing efficient code and solving complex problems. In this module, we'll cover common data structures and their implementations in JavaScript.

Arrays (Advanced)

Arrays are one of the most fundamental data structures. In JavaScript, arrays are dynamic and can hold values of different types. We've covered basic array operations in previous modules, so let's focus on more advanced concepts.

Multi-dimensional Arrays

// Creating a 2D array
const matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];

// Accessing elements
console.log(matrix[0][0]); // 1
console.log(matrix[1][2]); // 6

// Iterating over a 2D array
for (let i = 0; i < matrix.length; i++) {
    for (let j = 0; j < matrix[i].length; j++) {
        console.log(matrix[i][j]);
    }
}

// Creating a 3D array
const cube = [
    [
        [1, 2],
        [3, 4]
    ],
    [
        [5, 6],
        [7, 8]
    ]
];

console.log(cube[0][1][0]); // 3
Sparse Arrays

// Creating a sparse array
const sparseArray = [];
sparseArray[0] = "a";
sparseArray[5] = "f";

console.log(sparseArray.length); // 6
console.log(sparseArray); // ["a", empty × 4, "f"]

// Checking if an index is empty
console.log(0 in sparseArray); // true
console.log(1 in sparseArray); // false

// Iterating over a sparse array
for (let i = 0; i < sparseArray.length; i++) {
    if (i in sparseArray) {
        console.log(i, sparseArray[i]);
    }
}
Linked Lists

A linked list is a linear data structure where elements are not stored at contiguous memory locations. Each element (node) contains a value and a pointer to the next node.

Singly Linked List

class Node {
    constructor(data) {
        this.data = data;
        this.next = null;
    }
}

class LinkedList {
    constructor() {
        this.head = null;
        this.size = 0;
    }
    
    // Add to the beginning of the list
    prepend(data) {
        const newNode = new Node(data);
        newNode.next = this.head;
        this.head = newNode;
        this.size++;
    }
    
    // Add to the end of the list
    append(data) {
        const newNode = new Node(data);
        
        if (!this.head) {
            this.head = newNode;
        } else {
            let current = this.head;
            while (current.next) {
                current = current.next;
            }
            current.next = newNode;
        }
        this.size++;
    }
    
    // Insert at a specific index
    insertAt(data, index) {
        if (index < 0 || index > this.size) {
            return false;
        }
        
        if (index === 0) {
            this.prepend(data);
        } else {
            const newNode = new Node(data);
            let current = this.head;
            let previous;
            
            for (let i = 0; i < index; i++) {
                previous = current;
                current = current.next;
            }
            
            newNode.next = current;
            previous.next = newNode;
            this.size++;
        }
        return true;
    }
    
    // Remove at a specific index
    removeAt(index) {
        if (index < 0 || index >= this.size) {
            return null;
        }
        
        let current = this.head;
        let previous;
        
        if (index === 0) {
            this.head = current.next;
        } else {
            for (let i = 0; i < index; i++) {
                previous = current;
                current = current.next;
            }
            previous.next = current.next;
        }
        
        this.size--;
        return current.data;
    }
    
    // Get the data at a specific index
    get(index) {
        if (index < 0 || index >= this.size) {
            return null;
        }
        
        let current = this.head;
        for (let i = 0; i < index; i++) {
            current = current.next;
        }
        
        return current.data;
    }
    
    // Convert to array
    toArray() {
        const result = [];
        let current = this.head;
        
        while (current) {
            result.push(current.data);
            current = current.next;
        }
        
        return result;
    }
    
    // Print the list
    print() {
        let current = this.head;
        let result = "";
        
        while (current) {
            result += current.data + " -> ";
            current = current.next;
        }
        
        console.log(result + "null");
    }
}

// Using the linked list
const list = new LinkedList();
list.append(1);
list.append(2);
list.append(3);
list.prepend(0);
list.insertAt(1.5, 2);
list.print(); // "0 -> 1 -> 1.5 -> 2 -> 3 -> null"
console.log(list.get(2)); // 1.5
list.removeAt(2);
list.print(); // "0 -> 1 -> 2 -> 3 -> null"
console.log(list.toArray()); // [0, 1, 2, 3]
Doubly Linked List

class DoublyNode {
    constructor(data) {
        this.data = data;
        this.next = null;
        this.prev = null;
    }
}

class DoublyLinkedList {
    constructor() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }
    
    // Add to the beginning of the list
    prepend(data) {
        const newNode = new DoublyNode(data);
        
        if (!this.head) {
            this.head = newNode;
            this.tail = newNode;
        } else {
            newNode.next = this.head;
            this.head.prev = newNode;
            this.head = newNode;
        }
        
        this.size++;
    }
    
    // Add to the end of the list
    append(data) {
        const newNode = new DoublyNode(data);
        
        if (!this.tail) {
            this.head = newNode;
            this.tail = newNode;
        } else {
            newNode.prev = this.tail;
            this.tail.next = newNode;
            this.tail = newNode;
        }
        
        this.size++;
    }
    
    // Remove at a specific index
    removeAt(index) {
        if (index < 0 || index >= this.size) {
            return null;
        }
        
        let current = this.head;
        
        if (index === 0) {
            this.head = current.next;
            if (this.head) {
                this.head.prev = null;
            } else {
                this.tail = null;
            }
        } else if (index === this.size - 1) {
            current = this.tail;
            this.tail = current.prev;
            this.tail.next = null;
        } else {
            for (let i = 0; i < index; i++) {
                current = current.next;
            }
            
            current.prev.next = current.next;
            current.next.prev = current.prev;
        }
        
        this.size--;
        return current.data;
    }
    
    // Print the list forward
    printForward() {
        let current = this.head;
        let result = "";
        
        while (current) {
            result += current.data + " <-> ";
            current = current.next;
        }
        
        console.log(result + "null");
    }
    
    // Print the list backward
    printBackward() {
        let current = this.tail;
        let result = "";
        
        while (current) {
            result += current.data + " <-> ";
            current = current.prev;
        }
        
        console.log(result + "null");
    }
}

// Using the doubly linked list
const doublyList = new DoublyLinkedList();
doublyList.append(1);
doublyList.append(2);
doublyList.append(3);
doublyList.prepend(0);
doublyList.printForward(); // "0 <-> 1 <-> 2 <-> 3 <-> null"
doublyList.printBackward(); // "3 <-> 2 <-> 1 <-> 0 <-> null"
doublyList.removeAt(2);
doublyList.printForward(); // "0 <-> 1 <-> 3 <-> null"
Stacks

A stack is a linear data structure that follows the Last-In-First-Out (LIFO) principle. Elements can only be added and removed from the top of the stack.


class Stack {
    constructor() {
        this.items = [];
    }
    
    // Add an element to the top of the stack
    push(element) {
        this.items.push(element);
    }
    
    // Remove and return the top element of the stack
    pop() {
        if (this.isEmpty()) {
            return null;
        }
        return this.items.pop();
    }
    
    // Return the top element of the stack without removing it
    peek() {
        if (this.isEmpty()) {
            return null;
        }
        return this.items[this.items.length - 1];
    }
    
    // Check if the stack is empty
    isEmpty() {
        return this.items.length === 0;
    }
    
    // Return the size of the stack
    size() {
        return this.items.length;
    }
    
    // Clear the stack
    clear() {
        this.items = [];
    }
    
    // Print the stack
    print() {
        console.log(this.items.toString());
    }
}

// Using the stack
const stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(3);
stack.print(); // "1,2,3"
console.log(stack.pop()); // 3
console.log(stack.peek()); // 2
console.log(stack.size()); // 2
Queues

A queue is a linear data structure that follows the First-In-First-Out (FIFO) principle. Elements are added at the rear and removed from the front.


class Queue {
    constructor() {
        this.items = [];
    }
    
    // Add an element to the rear of the queue
    enqueue(element) {
        this.items.push(element);
    }
    
    // Remove and return the front element of the queue
    dequeue() {
        if (this.isEmpty()) {
            return null;
        }
        return this.items.shift();
    }
    
    // Return the front element of the queue without removing it
    front() {
        if (this.isEmpty()) {
            return null;
        }
        return this.items[0];
    }
    
    // Check if the queue is empty
    isEmpty() {
        return this.items.length === 0;
    }
    
    // Return the size of the queue
    size() {
        return this.items.length;
    }
    
    // Clear the queue
    clear() {
        this.items = [];
    }
    
    // Print the queue
    print() {
        console.log(this.items.toString());
    }
}

// Using the queue
const queue = new Queue();
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
queue.print(); // "1,2,3"
console.log(queue.dequeue()); // 1
console.log(queue.front()); // 2
console.log(queue.size()); // 2
Priority Queue

A priority queue is a special type of queue where each element has a priority associated with it. Elements with higher priority are served before elements with lower priority.


class PriorityQueue {
    constructor() {
        this.items = [];
    }
    
    // Add an element with a priority
    enqueue(element, priority) {
        const queueElement = { element, priority };
        let added = false;
        
        for (let i = 0; i < this.items.length; i++) {
            if (queueElement.priority < this.items[i].priority) {
                this.items.splice(i, 0, queueElement);
                added = true;
                break;
            }
        }
        
        if (!added) {
            this.items.push(queueElement);
        }
    }
    
    // Remove and return the element with the highest priority
    dequeue() {
        if (this.isEmpty()) {
            return null;
        }
        return this.items.shift().element;
    }
    
    // Return the element with the highest priority without removing it
    front() {
        if (this.isEmpty()) {
            return null;
        }
        return this.items[0].element;
    }
    
    // Check if the queue is empty
    isEmpty() {
        return this.items.length === 0;
    }
    
    // Return the size of the queue
    size() {
        return this.items.length;
    }
    
    // Print the queue
    print() {
        const result = this.items.map(item => `${item.element}(${item.priority})`);
        console.log(result.toString());
    }
}

// Using the priority queue
const priorityQueue = new PriorityQueue();
priorityQueue.enqueue("John", 2);
priorityQueue.enqueue("Jane", 1);
priorityQueue.enqueue("Bob", 3);
priorityQueue.print(); // "Jane(1),John(2),Bob(3)"
console.log(priorityQueue.dequeue()); // "Jane"
console.log(priorityQueue.front()); // "John"
Trees

A tree is a hierarchical data structure with a root node and child nodes. Each node contains a value and references to its child nodes.

Binary Tree

class TreeNode {
    constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

class BinaryTree {
    constructor() {
        this.root = null;
    }
    
    // Insert a value into the tree
    insert(value) {
        const newNode = new TreeNode(value);
        
        if (!this.root) {
            this.root = newNode;
        } else {
            this.insertNode(this.root, newNode);
        }
    }
    
    insertNode(node, newNode) {
        if (newNode.value < node.value) {
            if (!node.left) {
                node.left = newNode;
            } else {
                this.insertNode(node.left, newNode);
            }
        } else {
            if (!node.right) {
                node.right = newNode;
            } else {
                this.insertNode(node.right, newNode);
            }
        }
    }
    
    // Search for a value in the tree
    search(value) {
        return this.searchNode(this.root, value);
    }
    
    searchNode(node, value) {
        if (!node) {
            return false;
        }
        
        if (value < node.value) {
            return this.searchNode(node.left, value);
        } else if (value > node.value) {
            return this.searchNode(node.right, value);
        } else {
            return true;
        }
    }
    
    // In-order traversal (left, root, right)
    inOrder(callback) {
        this.inOrderTraversal(this.root, callback);
    }
    
    inOrderTraversal(node, callback) {
        if (node) {
            this.inOrderTraversal(node.left, callback);
            callback(node.value);
            this.inOrderTraversal(node.right, callback);
        }
    }
    
    // Pre-order traversal (root, left, right)
    preOrder(callback) {
        this.preOrderTraversal(this.root, callback);
    }
    
    preOrderTraversal(node, callback) {
        if (node) {
            callback(node.value);
            this.preOrderTraversal(node.left, callback);
            this.preOrderTraversal(node.right, callback);
        }
    }
    
    // Post-order traversal (left, right, root)
    postOrder(callback) {
        this.postOrderTraversal(this.root, callback);
    }
    
    postOrderTraversal(node, callback) {
        if (node) {
            this.postOrderTraversal(node.left, callback);
            this.postOrderTraversal(node.right, callback);
            callback(node.value);
        }
    }
}

// Using the binary tree
const tree = new BinaryTree();
tree.insert(7);
tree.insert(4);
tree.insert(9);
tree.insert(1);
tree.insert(5);
tree.insert(8);
tree.insert(10);

console.log(tree.search(5)); // true
console.log(tree.search(6)); // false

console.log("In-order traversal:");
tree.inOrder(value => console.log(value)); // 1, 4, 5, 7, 8, 9, 10

console.log("Pre-order traversal:");
tree.preOrder(value => console.log(value)); // 7, 4, 1, 5, 9, 8, 10

console.log("Post-order traversal:");
tree.postOrder(value => console.log(value)); // 1, 5, 4, 8, 10, 9, 7
Graphs

A graph is a non-linear data structure consisting of vertices (nodes) and edges. Graphs can be directed or undirected, weighted or unweighted.

Adjacency List Representation

class Graph {
    constructor() {
        this.adjacencyList = {};
    }
    
    // Add a vertex
    addVertex(vertex) {
        if (!this.adjacencyList[vertex]) {
            this.adjacencyList[vertex] = [];
        }
    }
    
    // Add an edge (undirected)
    addEdge(vertex1, vertex2) {
        if (!this.adjacencyList[vertex1] || !this.adjacencyList[vertex2]) {
            return false;
        }
        
        this.adjacencyList[vertex1].push(vertex2);
        this.adjacencyList[vertex2].push(vertex1);
        return true;
    }
    
    // Add a directed edge
    addDirectedEdge(from, to) {
        if (!this.adjacencyList[from] || !this.adjacencyList[to]) {
            return false;
        }
        
        this.adjacencyList[from].push(to);
        return true;
    }
    
    // Remove an edge
    removeEdge(vertex1, vertex2) {
        this.adjacencyList[vertex1] = this.adjacencyList[vertex1].filter(
            v => v !== vertex2
        );
        this.adjacencyList[vertex2] = this.adjacencyList[vertex2].filter(
            v => v !== vertex1
        );
    }
    
    // Remove a vertex
    removeVertex(vertex) {
        while (this.adjacencyList[vertex].length) {
            const adjacentVertex = this.adjacencyList[vertex].pop();
            this.removeEdge(vertex, adjacentVertex);
        }
        delete this.adjacencyList[vertex];
    }
    
    // Depth First Search (DFS)
    dfs(start) {
        const result = [];
        const visited = {};
        const stack = [start];
        visited[start] = true;
        
        while (stack.length) {
            const vertex = stack.pop();
            result.push(vertex);
            
            this.adjacencyList[vertex].forEach(neighbor => {
                if (!visited[neighbor]) {
                    visited[neighbor] = true;
                    stack.push(neighbor);
                }
            });
        }
        
        return result;
    }
    
    // Breadth First Search (BFS)
    bfs(start) {
        const result = [];
        const visited = {};
        const queue = [start];
        visited[start] = true;
        
        while (queue.length) {
            const vertex = queue.shift();
            result.push(vertex);
            
            this.adjacencyList[vertex].forEach(neighbor => {
                if (!visited[neighbor]) {
                    visited[neighbor] = true;
                    queue.push(neighbor);
                }
            });
        }
        
        return result;
    }
    
    // Print the graph
    print() {
        for (const vertex in this.adjacencyList) {
            console.log(vertex + " -> " + this.adjacencyList[vertex].join(", "));
        }
    }
}

// Using the graph
const graph = new Graph();
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addVertex("D");
graph.addVertex("E");
graph.addVertex("F");

graph.addEdge("A", "B");
graph.addEdge("A", "C");
graph.addEdge("B", "D");
graph.addEdge("C", "E");
graph.addEdge("D", "E");
graph.addEdge("D", "F");
graph.addEdge("E", "F");

graph.print();
// A -> B, C
// B -> A, D
// C -> A, E
// D -> B, E, F
// E -> C, D, F
// F -> D, E

console.log("DFS:", graph.dfs("A")); // ["A", "C", "E", "F", "D", "B"]
console.log("BFS:", graph.bfs("A")); // ["A", "B", "C", "D", "E", "F"]
Hash Tables

A hash table is a data structure that maps keys to values for highly efficient lookup. It uses a hash function to compute an index into an array of buckets or slots.


class HashTable {
    constructor(size = 53) {
        this.keyMap = new Array(size);
    }
    
    // Hash function
    _hash(key) {
        let total = 0;
        let WEIRD_PRIME = 31;
        
        for (let i = 0; i < Math.min(key.length, 100); i++) {
            let char = key[i];
            let value = char.charCodeAt(0) - 96;
            total = (total * WEIRD_PRIME + value) % this.keyMap.length;
        }
        
        return total;
    }
    
    // Set a key-value pair
    set(key, value) {
        const index = this._hash(key);
        
        if (!this.keyMap[index]) {
            this.keyMap[index] = [];
        }
        
        // Check if the key already exists and update it
        for (let i = 0; i < this.keyMap[index].length; i++) {
            if (this.keyMap[index][i][0] === key) {
                this.keyMap[index][i][1] = value;
                return true;
            }
        }
        
        // Add a new key-value pair
        this.keyMap[index].push([key, value]);
        return true;
    }
    
    // Get a value by key
    get(key) {
        const index = this._hash(key);
        
        if (!this.keyMap[index]) {
            return undefined;
        }
        
        for (let i = 0; i < this.keyMap[index].length; i++) {
            if (this.keyMap[index][i][0] === key) {
                return this.keyMap[index][i][1];
            }
        }
        
        return undefined;
    }
    
    // Remove a key-value pair
    remove(key) {
        const index = this._hash(key);
        
        if (!this.keyMap[index]) {
            return false;
        }
        
        for (let i = 0; i < this.keyMap[index].length; i++) {
            if (this.keyMap[index][i][0] === key) {
                this.keyMap[index].splice(i, 1);
                return true;
            }
        }
        
        return false;
    }
    
    // Get all keys
    keys() {
        const keys = [];
        
        for (let i = 0; i < this.keyMap.length; i++) {
            if (this.keyMap[i]) {
                for (let j = 0; j < this.keyMap[i].length; j++) {
                    keys.push(this.keyMap[i][j][0]);
                }
            }
        }
        
        return keys;
    }
    
    // Get all values
    values() {
        const values = [];
        
        for (let i = 0; i < this.keyMap.length; i++) {
            if (this.keyMap[i]) {
                for (let j = 0; j < this.keyMap[i].length; j++) {
                    values.push(this.keyMap[i][j][1]);
                }
            }
        }
        
        return values;
    }
    
    // Print the hash table
    print() {
        for (let i = 0; i < this.keyMap.length; i++) {
            if (this.keyMap[i]) {
                console.log(i, this.keyMap[i]);
            }
        }
    }
}

// Using the hash table
const hashTable = new HashTable();
hashTable.set("name", "John");
hashTable.set("age", 25);
hashTable.set("email", "john@example.com");

console.log(hashTable.get("name")); // "John"
console.log(hashTable.get("age")); // 25
console.log(hashTable.get("email")); // "john@example.com"

hashTable.remove("age");
console.log(hashTable.get("age")); // undefined

console.log(hashTable.keys()); // ["name", "email"]
console.log(hashTable.values()); // ["John", "john@example.com"]
Interactive Example

Try different data structures:

Data structure output will appear here...
Module 16: Algorithms
Introduction to Algorithms

An algorithm is a step-by-step procedure for solving a problem or completing a task. In computer science, algorithms are fundamental to writing efficient code. Understanding algorithms helps you choose the right approach for a problem and optimize your code for better performance.

Algorithms are often analyzed in terms of time complexity (how the runtime grows with input size) and space complexity (how much memory is used). Big O notation is commonly used to describe these complexities.

Sorting Algorithms

Sorting algorithms arrange elements in a specific order. Different sorting algorithms have different time and space complexities, making them suitable for different scenarios.

Bubble Sort

Bubble sort repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. It has a time complexity of O(n²).


function bubbleSort(arr) {
    const n = arr.length;
    
    for (let i = 0; i < n - 1; i++) {
        let swapped = false;
        
        for (let j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                // Swap elements
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                swapped = true;
            }
        }
        
        // If no swapping occurred, the array is sorted
        if (!swapped) break;
    }
    
    return arr;
}

// Using bubble sort
const unsortedArray = [64, 34, 25, 12, 22, 11, 90];
console.log(bubbleSort([...unsortedArray])); // [11, 12, 22, 25, 34, 64, 90]
Selection Sort

Selection sort divides the input into a sorted and an unsorted region, and repeatedly selects the smallest element from the unsorted region and moves it to the sorted region. It has a time complexity of O(n²).


function selectionSort(arr) {
    const n = arr.length;
    
    for (let i = 0; i < n - 1; i++) {
        let minIndex = i;
        
        // Find the minimum element in the unsorted part
        for (let j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        
        // Swap the found minimum element with the first element
        [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
    
    return arr;
}

// Using selection sort
const unsortedArray = [64, 34, 25, 12, 22, 11, 90];
console.log(selectionSort([...unsortedArray])); // [11, 12, 22, 25, 34, 64, 90]
Insertion Sort

Insertion sort builds the final sorted array one item at a time. It's much less efficient on large lists than more advanced algorithms like quicksort, heapsort, or merge sort. It has a time complexity of O(n²).


function insertionSort(arr) {
    for (let i = 1; i < arr.length; i++) {
        let key = arr[i];
        let j = i - 1;
        
        // Move elements of arr[0..i-1] that are greater than key
        // to one position ahead of their current position
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        
        arr[j + 1] = key;
    }
    
    return arr;
}

// Using insertion sort
const unsortedArray = [64, 34, 25, 12, 22, 11, 90];
console.log(insertionSort([...unsortedArray])); // [11, 12, 22, 25, 34, 64, 90]
Merge Sort

Merge sort is a divide-and-conquer algorithm that divides the input array into two halves, recursively sorts them, and then merges the sorted halves. It has a time complexity of O(n log n).


function mergeSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }
    
    // Find the middle of the array
    const middle = Math.floor(arr.length / 2);
    
    // Divide the array into two halves
    const left = arr.slice(0, middle);
    const right = arr.slice(middle);
    
    // Recursively sort the two halves
    return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
    let result = [];
    let leftIndex = 0;
    let rightIndex = 0;
    
    // Compare elements from both arrays and add the smaller one to the result
    while (leftIndex < left.length && rightIndex < right.length) {
        if (left[leftIndex] < right[rightIndex]) {
            result.push(left[leftIndex]);
            leftIndex++;
        } else {
            result.push(right[rightIndex]);
            rightIndex++;
        }
    }
    
    // Add the remaining elements from the left array
    while (leftIndex < left.length) {
        result.push(left[leftIndex]);
        leftIndex++;
    }
    
    // Add the remaining elements from the right array
    while (rightIndex < right.length) {
        result.push(right[rightIndex]);
        rightIndex++;
    }
    
    return result;
}

// Using merge sort
const unsortedArray = [64, 34, 25, 12, 22, 11, 90];
console.log(mergeSort([...unsortedArray])); // [11, 12, 22, 25, 34, 64, 90]
Quick Sort

Quick sort is a divide-and-conquer algorithm that picks an element as a pivot and partitions the array around the pivot, placing smaller elements to the left and larger elements to the right. It has an average time complexity of O(n log n) but a worst-case time complexity of O(n²).


function quickSort(arr, left = 0, right = arr.length - 1) {
    if (left < right) {
        // Partition the array and get the pivot index
        const pivotIndex = partition(arr, left, right);
        
        // Recursively sort the elements before and after the pivot
        quickSort(arr, left, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, right);
    }
    
    return arr;
}

function partition(arr, left, right) {
    // Choose the rightmost element as the pivot
    const pivot = arr[right];
    
    // Index of the smaller element
    let i = left - 1;
    
    for (let j = left; j < right; j++) {
        // If the current element is smaller than the pivot
        if (arr[j] < pivot) {
            i++;
            // Swap arr[i] and arr[j]
            [arr[i], arr[j]] = [arr[j], arr[i]];
        }
    }
    
    // Swap arr[i+1] and arr[right] (the pivot)
    [arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];
    
    // Return the partition index
    return i + 1;
}

// Using quick sort
const unsortedArray = [64, 34, 25, 12, 22, 11, 90];
console.log(quickSort([...unsortedArray])); // [11, 12, 22, 25, 34, 64, 90]
Searching Algorithms

Searching algorithms are used to find a specific item in a collection of items. The efficiency of a search algorithm depends on the structure of the data and the specific requirements of the search.

Linear Search

Linear search sequentially checks each element of the list until a match is found or the whole list has been searched. It has a time complexity of O(n).


function linearSearch(arr, target) {
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] === target) {
            return i; // Return the index of the found element
        }
    }
    return -1; // Return -1 if the element is not found
}

// Using linear search
const array = [2, 4, 0, 1, 9, 5, 3, 7];
console.log(linearSearch(array, 5)); // 5
console.log(linearSearch(array, 8)); // -1
Binary Search

Binary search works on sorted arrays by repeatedly dividing the search interval in half. It has a time complexity of O(log n).


function binarySearch(arr, target) {
    let left = 0;
    let right = arr.length - 1;
    
    while (left <= right) {
        // Find the middle index
        const middle = Math.floor((left + right) / 2);
        
        // Check if the middle element is the target
        if (arr[middle] === target) {
            return middle;
        }
        
        // If the target is in the left half
        if (arr[middle] > target) {
            right = middle - 1;
        }
        // If the target is in the right half
        else {
            left = middle + 1;
        }
    }
    
    return -1; // Return -1 if the element is not found
}

// Using binary search
const sortedArray = [0, 1, 2, 3, 4, 5, 7, 9];
console.log(binarySearch(sortedArray, 5)); // 5
console.log(binarySearch(sortedArray, 6)); // -1
Recursive Algorithms

Recursive algorithms solve problems by breaking them down into smaller subproblems of the same type. The solution to the problem is then constructed from the solutions to the subproblems.

Factorial

function factorial(n) {
    // Base case
    if (n <= 1) {
        return 1;
    }
    
    // Recursive case
    return n * factorial(n - 1);
}

// Using the factorial function
console.log(factorial(5)); // 120
console.log(factorial(0)); // 1
Fibonacci

// Naive recursive Fibonacci (inefficient)
function fibonacci(n) {
    // Base cases
    if (n <= 1) {
        return n;
    }
    
    // Recursive case
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Optimized Fibonacci with memoization
function fibonacciMemo(n, memo = {}) {
    // Check if the result is already in the memo
    if (n in memo) {
        return memo[n];
    }
    
    // Base cases
    if (n <= 1) {
        return n;
    }
    
    // Recursive case with memoization
    memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo);
    return memo[n];
}

// Using the Fibonacci functions
console.log(fibonacci(10)); // 55
console.log(fibonacciMemo(10)); // 55
Dynamic Programming

Dynamic programming is a method for solving complex problems by breaking them down into simpler subproblems. It is applicable when the subproblems have overlapping sub-subproblems.

Longest Common Subsequence

function longestCommonSubsequence(str1, str2) {
    const m = str1.length;
    const n = str2.length;
    
    // Create a table to store results of subproblems
    const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
    
    // Fill the table in a bottom-up manner
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (str1[i - 1] === str2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    
    return dp[m][n];
}

// Using the LCS function
console.log(longestCommonSubsequence("AGGTAB", "GXTXAYB")); // 4 (GTAB)
Knapsack Problem

function knapsack(weights, values, capacity) {
    const n = weights.length;
    
    // Create a table to store results of subproblems
    const dp = Array(n + 1).fill().map(() => Array(capacity + 1).fill(0));
    
    // Fill the table in a bottom-up manner
    for (let i = 1; i <= n; i++) {
        for (let w = 1; w <= capacity; w++) {
            if (weights[i - 1] <= w) {
                dp[i][w] = Math.max(
                    values[i - 1] + dp[i - 1][w - weights[i - 1]],
                    dp[i - 1][w]
                );
            } else {
                dp[i][w] = dp[i - 1][w];
            }
        }
    }
    
    return dp[n][capacity];
}

// Using the knapsack function
const weights = [1, 3, 4, 5];
const values = [1, 4, 5, 7];
const capacity = 7;
console.log(knapsack(weights, values, capacity)); // 9
Greedy Algorithms

Greedy algorithms make locally optimal choices at each step with the hope of finding a global optimum. They are often simpler to implement than dynamic programming solutions but don't always produce the optimal solution.

Activity Selection Problem

function activitySelection(start, end) {
    const n = start.length;
    
    // Create an array of activities with start and end times
    const activities = [];
    for (let i = 0; i < n; i++) {
        activities.push({
            start: start[i],
            end: end[i],
            index: i
        });
    }
    
    // Sort activities by end time
    activities.sort((a, b) => a.end - b.end);
    
    // Select the first activity
    const selected = [activities[0].index];
    let lastEnd = activities[0].end;
    
    // Select the rest of the activities
    for (let i = 1; i < n; i++) {
        if (activities[i].start >= lastEnd) {
            selected.push(activities[i].index);
            lastEnd = activities[i].end;
        }
    }
    
    return selected;
}

// Using the activity selection function
const start = [1, 3, 0, 5, 8, 5];
const end = [2, 4, 6, 7, 9, 9];
console.log(activitySelection(start, end)); // [0, 1, 3, 4]
Interactive Example

Try different algorithms:

Algorithm output will appear here...
Module 17: JavaScript Best Practices
Introduction to Best Practices

Following best practices is essential for writing clean, efficient, and maintainable JavaScript code. These practices help you avoid common pitfalls, improve performance, and make your code easier to understand and modify.

In this module, we'll cover various best practices for JavaScript development, including code organization, performance optimization, security, and more.

Code Organization

Well-organized code is easier to read, understand, and maintain. Here are some best practices for organizing your JavaScript code:

Use Modules

Break your code into modules with single responsibilities. This makes your code more modular, reusable, and easier to test.


// math.js
export const PI = 3.14159;

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

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

// main.js
import { PI, add, multiply } from './math.js';

console.log(PI);
console.log(add(5, 3));
console.log(multiply(5, 3));
Use Descriptive Names

Use meaningful names for variables, functions, and classes. This makes your code self-documenting and easier to understand.


// Bad
const d = new Date();
const u = users.filter(u => u.age > 18);

// Good
const currentDate = new Date();
const adultUsers = users.filter(user => user.age > 18);
Keep Functions Small

Functions should do one thing and do it well. Small functions are easier to test, understand, and reuse.


// Bad
function processUsers(users) {
    const validUsers = [];
    for (let i = 0; i < users.length; i++) {
        if (users[i].age >= 18 && users[i].email) {
            validUsers.push(users[i]);
        }
    }
    
    const result = [];
    for (let i = 0; i < validUsers.length; i++) {
        result.push({
            id: validUsers[i].id,
            name: validUsers[i].name,
            email: validUsers[i].email
        });
    }
    
    return result;
}

// Good
function isAdult(user) {
    return user.age >= 18;
}

function hasEmail(user) {
    return !!user.email;
}

function isValidUser(user) {
    return isAdult(user) && hasEmail(user);
}

function formatUser(user) {
    return {
        id: user.id,
        name: user.name,
        email: user.email
    };
}

function processUsers(users) {
    return users
        .filter(isValidUser)
        .map(formatUser);
}
Performance Optimization

Optimizing your JavaScript code can significantly improve the performance of your web applications. Here are some best practices for performance optimization:

Minimize DOM Manipulation

DOM manipulation is expensive. Minimize the number of DOM operations by batching changes or using document fragments.


// Bad
const list = document.getElementById("list");
for (let i = 0; i < 1000; i++) {
    const item = document.createElement("li");
    item.textContent = "Item " + i;
    list.appendChild(item);
}

// Good
const list = document.getElementById("list");
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    const item = document.createElement("li");
    item.textContent = "Item " + i;
    fragment.appendChild(item);
}
list.appendChild(fragment);
Use Event Delegation

Instead of adding an event listener to each element, add a single event listener to a parent element and use event delegation.


// Bad
const buttons = document.querySelectorAll(".button");
buttons.forEach(button => {
    button.addEventListener("click", function() {
        console.log("Button clicked");
    });
});

// Good
document.addEventListener("click", function(event) {
    if (event.target.matches(".button")) {
        console.log("Button clicked");
    }
});
Use RequestAnimationFrame for Animations

When creating animations, use requestAnimationFrame instead of setInterval or setTimeout for smoother animations and better performance.


// Bad
let position = 0;
setInterval(() => {
    position += 1;
    element.style.left = position + "px";
}, 16);

// Good
let position = 0;
function animate() {
    position += 1;
    element.style.left = position + "px";
    requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Optimize Loops

Optimize loops by minimizing the work done inside the loop and using efficient iteration methods.


// Bad
for (let i = 0; i < array.length; i++) {
    // array.length is calculated in each iteration
    console.log(array[i]);
}

// Good
const length = array.length;
for (let i = 0; i < length; i++) {
    console.log(array[i]);
}

// Even better (for arrays)
for (const item of array) {
    console.log(item);
}
Error Handling

Proper error handling makes your code more robust and user-friendly. Here are some best practices for error handling:

Use Try-Catch for Error-Prone Operations

try {
    const data = JSON.parse(jsonString);
    processData(data);
} catch (error) {
    console.error("Error parsing JSON:", error);
    showUserMessage("Invalid data format");
}
Validate Input

function processUser(user) {
    if (!user || typeof user !== "object") {
        throw new Error("Invalid user object");
    }
    
    if (!user.name || typeof user.name !== "string") {
        throw new Error("Invalid user name");
    }
    
    if (!user.age || typeof user.age !== "number" || user.age < 0) {
        throw new Error("Invalid user age");
    }
    
    // Process user
}
Handle Promises Properly

// Bad
fetch("/api/data")
    .then(response => response.json())
    .then(data => processData(data)); // No error handling

// Good
fetch("/api/data")
    .then(response => {
        if (!response.ok) {
            throw new Error("Network response was not ok");
        }
        return response.json();
    })
    .then(data => processData(data))
    .catch(error => {
        console.error("Error fetching data:", error);
        showUserMessage("Failed to load data");
    });

// Even better with async/await
async function fetchData() {
    try {
        const response = await fetch("/api/data");
        
        if (!response.ok) {
            throw new Error("Network response was not ok");
        }
        
        const data = await response.json();
        return processData(data);
    } catch (error) {
        console.error("Error fetching data:", error);
        showUserMessage("Failed to load data");
    }
}
Security

Security is crucial in web development. Here are some best practices for writing secure JavaScript code:

Avoid eval()

// Bad - eval can execute arbitrary code
const code = "alert('XSS attack')";
eval(code);

// Good - Use JSON.parse for parsing JSON
const jsonString = '{"name": "John", "age": 25}';
const data = JSON.parse(jsonString);
Sanitize User Input

// Bad - Directly using user input
element.innerHTML = userInput;

// Good - Sanitize user input
element.textContent = userInput;

// If you need to use HTML, sanitize it first
function sanitizeHTML(str) {
    return str.replace(/&/g, "&")
              .replace(//g, ">")
              .replace(/"/g, """)
              .replace(/'/g, "'");
}

element.innerHTML = sanitizeHTML(userInput);
Use HTTPS

// Bad - Using HTTP
fetch("http://api.example.com/data");

// Good - Using HTTPS
fetch("https://api.example.com/data");
Testing

Testing is essential for ensuring the quality and reliability of your code. Here are some best practices for testing JavaScript code:

Write Unit Tests

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

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

function testAdd() {
    if (add(2, 3) !== 5) {
        throw new Error("add(2, 3) should return 5");
    }
    
    if (add(-1, 1) !== 0) {
        throw new Error("add(-1, 1) should return 0");
    }
    
    console.log("All add tests passed");
}

testAdd();
Use Test Frameworks

// Using Jest
import { add } from "./math.js";

test("adds 1 + 2 to equal 3", () => {
    expect(add(1, 2)).toBe(3);
});

test("adds -1 + 1 to equal 0", () => {
    expect(add(-1, 1)).toBe(0);
});
Code Style and Formatting

Consistent code style and formatting make your code easier to read and maintain. Here are some best practices:

Use a Linter

Use a linter like ESLint to enforce consistent code style and catch potential errors.

Use a Formatter

Use a formatter like Prettier to automatically format your code according to a consistent style.

Follow Naming Conventions

// Variables and functions: camelCase
const userName = "John";
function calculateTotal() {}

// Classes: PascalCase
class UserAccount {}

// Constants: UPPER_SNAKE_CASE
const API_URL = "https://api.example.com";

// Private properties: underscore prefix
class User {
    constructor() {
        this._id = null;
    }
}
Documentation

Good documentation makes your code easier to understand and use. Here are some best practices for documenting your code:

Use JSDoc

/**
 * Calculates the sum of two numbers.
 * @param {number} a - The first number.
 * @param {number} b - The second number.
 * @returns {number} The sum of a and b.
 * @example
 * // returns 5
 * add(2, 3);
 */
function add(a, b) {
    return a + b;
}
Write README Files

Write a README file for your projects that explains what the project does, how to install and use it, and how to contribute.

Interactive Example

Try applying best practices:

Best practices output will appear here...
Module 18: Final Project
Project Overview

For the final project, you'll create a comprehensive web application that demonstrates all the JavaScript concepts learned throughout the course. This project will include DOM manipulation, event handling, asynchronous operations, and a polished user interface.

The project will be a task management application that allows users to create, read, update, and delete tasks. It will also include features like filtering, sorting, and local storage for persistence.

Project Structure

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Manager</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header>
        <h1>Task Manager</h1>
    </header>
    
    <main>
        <section id="task-form">
            <form>
                <div>
                    <label for="title">Title:</label>
                    <input type="text" id="title" required>
                </div>
                <div>
                    <label for="description">Description:</label>
                    <textarea id="description"></textarea>
                </div>
                <div>
                    <label for="due-date">Due Date:</label>
                    <input type="date" id="due-date">
                </div>
                <div>
                    <label for="priority">Priority:</label>
                    <select id="priority">
                        <option value="low">Low</option>
                        <option value="medium">Medium</option>
                        <option value="high">High</option>
                    </select>
                </div>
                <button type="submit">Add Task</button>
            </form>
        </section>
        
        <section id="task-filters">
            <div>
                <label for="filter-status">Filter by Status:</label>
                <select id="filter-status">
                    <option value="all">All</option>
                    <option value="pending">Pending</option>
                    <option value="completed">Completed</option>
                </select>
            </div>
            <div>
                <label for="filter-priority">Filter by Priority:</label>
                <select id="filter-priority">
                    <option value="all">All</option>
                    <option value="low">Low</option>
                    <option value="medium">Medium</option>
                    <option value="high">High</option>
                </select>
            </div>
            <div>
                <label for="sort-by">Sort By:</label>
                <select id="sort-by">
                    <option value="due-date">Due Date</option>
                    <option value="priority">Priority</option>
                    <option value="title">Title</option>
                </select>
            </div>
        </section>
        
        <section id="task-list">
            <h2>Tasks</h2>
            <div id="tasks"></div>
        </section>
    </main>
    
    <footer>
        <p>© 2023 Task Manager</p>
    </footer>
    
    <script type="module" src="app.js"></script>
</body>
</html>
CSS Styling

/* styles.css */
:root {
    --primary-color: #4a6cf7;
    --secondary-color: #6c757d;
    --success-color: #28a745;
    --danger-color: #dc3545;
    --warning-color: #ffc107;
    --light-color: #f8f9fa;
    --dark-color: #343a40;
    --border-radius: 0.25rem;
    --box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    line-height: 1.6;
    color: var(--dark-color);
    background-color: var(--light-color);
}

header {
    background-color: var(--primary-color);
    color: white;
    padding: 1rem;
    text-align: center;
}

main {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
}

section {
    background-color: white;
    border-radius: var(--border-radius);
    box-shadow: var(--box-shadow);
    padding: 1.5rem;
    margin-bottom: 2rem;
}

form {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 1rem;
}

form div {
    display: flex;
    flex-direction: column;
}

label {
    margin-bottom: 0.5rem;
    font-weight: 500;
}

input, textarea, select, button {
    padding: 0.75rem;
    border: 1px solid #ced4da;
    border-radius: var(--border-radius);
    font-size: 1rem;
}

button {
    background-color: var(--primary-color);
    color: white;
    border: none;
    cursor: pointer;
    font-weight: 500;
    transition: background-color 0.2s;
}

button:hover {
    background-color: #3a5bd9;
}

#task-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
}

#task-filters > div {
    flex: 1;
    min-width: 200px;
}

.task {
    border: 1px solid #ced4da;
    border-radius: var(--border-radius);
    padding: 1rem;
    margin-bottom: 1rem;
    position: relative;
}

.task.completed {
    background-color: #f8f9fa;
    text-decoration: line-through;
}

.task-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 0.5rem;
}

.task-title {
    font-weight: 600;
    font-size: 1.1rem;
}

.task-priority {
    padding: 0.25rem 0.5rem;
    border-radius: var(--border-radius);
    font-size: 0.8rem;
    font-weight: 500;
}

.priority-high {
    background-color: var(--danger-color);
    color: white;
}

.priority-medium {
    background-color: var(--warning-color);
    color: var(--dark-color);
}

.priority-low {
    background-color: var(--success-color);
    color: white;
}

.task-description {
    margin-bottom: 0.5rem;
}

.task-meta {
    display: flex;
    justify-content: space-between;
    font-size: 0.9rem;
    color: var(--secondary-color);
}

.task-actions {
    position: absolute;
    top: 1rem;
    right: 1rem;
    display: flex;
    gap: 0.5rem;
}

.task-actions button {
    padding: 0.25rem 0.5rem;
    font-size: 0.8rem;
}

.edit-btn {
    background-color: var(--warning-color);
}

.delete-btn {
    background-color: var(--danger-color);
}

footer {
    text-align: center;
    padding: 1rem;
    color: var(--secondary-color);
}

@media (max-width: 768px) {
    main {
        padding: 1rem;
    }
    
    form {
        grid-template-columns: 1fr;
    }
    
    #task-filters {
        flex-direction: column;
    }
    
    #task-filters > div {
        min-width: auto;
    }
}
JavaScript Implementation

// app.js
// Task class
class Task {
    constructor(title, description, dueDate, priority, status = 'pending') {
        this.id = Date.now().toString();
        this.title = title;
        this.description = description;
        this.dueDate = dueDate;
        this.priority = priority;
        this.status = status;
    }
    
    static fromJSON(json) {
        const task = new Task(
            json.title,
            json.description,
            json.dueDate,
            json.priority,
            json.status
        );
        task.id = json.id;
        return task;
    }
}

// Task Manager class
class TaskManager {
    constructor() {
        this.tasks = this.loadTasks();
        this.filterStatus = 'all';
        this.filterPriority = 'all';
        this.sortBy = 'due-date';
        this.init();
    }
    
    init() {
        this.bindEvents();
        this.renderTasks();
    }
    
    bindEvents() {
        // Form submission
        document.getElementById('task-form').addEventListener('submit', (e) => {
            e.preventDefault();
            this.addTask();
        });
        
        // Filter changes
        document.getElementById('filter-status').addEventListener('change', (e) => {
            this.filterStatus = e.target.value;
            this.renderTasks();
        });
        
        document.getElementById('filter-priority').addEventListener('change', (e) => {
            this.filterPriority = e.target.value;
            this.renderTasks();
        });
        
        // Sort change
        document.getElementById('sort-by').addEventListener('change', (e) => {
            this.sortBy = e.target.value;
            this.renderTasks();
        });
    }
    
    addTask() {
        const title = document.getElementById('title').value.trim();
        const description = document.getElementById('description').value.trim();
        const dueDate = document.getElementById('due-date').value;
        const priority = document.getElementById('priority').value;
        
        if (!title) {
            this.showMessage('Please enter a task title', 'error');
            return;
        }
        
        const task = new Task(title, description, dueDate, priority);
        this.tasks.push(task);
        this.saveTasks();
        this.renderTasks();
        this.resetForm();
        this.showMessage('Task added successfully', 'success');
    }
    
    updateTask(id, updatedTask) {
        const index = this.tasks.findIndex(task => task.id === id);
        if (index !== -1) {
            this.tasks[index] = { ...this.tasks[index], ...updatedTask };
            this.saveTasks();
            this.renderTasks();
            this.showMessage('Task updated successfully', 'success');
        }
    }
    
    deleteTask(id) {
        if (confirm('Are you sure you want to delete this task?')) {
            this.tasks = this.tasks.filter(task => task.id !== id);
            this.saveTasks();
            this.renderTasks();
            this.showMessage('Task deleted successfully', 'success');
        }
    }
    
    toggleTaskStatus(id) {
        const task = this.tasks.find(task => task.id === id);
        if (task) {
            task.status = task.status === 'pending' ? 'completed' : 'pending';
            this.saveTasks();
            this.renderTasks();
        }
    }
    
    getFilteredAndSortedTasks() {
        let filteredTasks = [...this.tasks];
        
        // Apply filters
        if (this.filterStatus !== 'all') {
            filteredTasks = filteredTasks.filter(task => task.status === this.filterStatus);
        }
        
        if (this.filterPriority !== 'all') {
            filteredTasks = filteredTasks.filter(task => task.priority === this.filterPriority);
        }
        
        // Apply sorting
        filteredTasks.sort((a, b) => {
            switch (this.sortBy) {
                case 'due-date':
                    return new Date(a.dueDate) - new Date(b.dueDate);
                case 'priority':
                    const priorityOrder = { high: 0, medium: 1, low: 2 };
                    return priorityOrder[a.priority] - priorityOrder[b.priority];
                case 'title':
                    return a.title.localeCompare(b.title);
                default:
                    return 0;
            }
        });
        
        return filteredTasks;
    }
    
    renderTasks() {
        const tasksContainer = document.getElementById('tasks');
        const tasks = this.getFilteredAndSortedTasks();
        
        if (tasks.length === 0) {
            tasksContainer.innerHTML = '

No tasks found

'; return; } tasksContainer.innerHTML = tasks.map(task => this.createTaskHTML(task)).join(''); // Add event listeners to task actions tasks.forEach(task => { const deleteBtn = document.getElementById(`delete-${task.id}`); const editBtn = document.getElementById(`edit-${task.id}`); const toggleBtn = document.getElementById(`toggle-${task.id}`); if (deleteBtn) { deleteBtn.addEventListener('click', () => this.deleteTask(task.id)); } if (editBtn) { editBtn.addEventListener('click', () => this.editTask(task.id)); } if (toggleBtn) { toggleBtn.addEventListener('click', () => this.toggleTaskStatus(task.id)); } }); } createTaskHTML(task) { const dueDate = task.dueDate ? new Date(task.dueDate).toLocaleDateString() : 'No due date'; const isCompleted = task.status === 'completed'; return `
${task.title}
${task.priority}
${task.description ? `
${task.description}
` : ''}
Due: ${dueDate}
Status: ${task.status}
`; } editTask(id) { const task = this.tasks.find(task => task.id === id); if (!task) return; // Populate form with task data document.getElementById('title').value = task.title; document.getElementById('description').value = task.description; document.getElementById('due-date').value = task.dueDate; document.getElementById('priority').value = task.priority; // Change form submission to update instead of add const form = document.getElementById('task-form'); form.removeEventListener('submit', this.addTaskHandler); const updateHandler = (e) => { e.preventDefault(); const title = document.getElementById('title').value.trim(); const description = document.getElementById('description').value.trim(); const dueDate = document.getElementById('due-date').value; const priority = document.getElementById('priority').value; if (!title) { this.showMessage('Please enter a task title', 'error'); return; } this.updateTask(id, { title, description, dueDate, priority }); // Reset form to add mode form.removeEventListener('submit', updateHandler); form.addEventListener('submit', this.addTaskHandler); this.resetForm(); }; form.addEventListener('submit', updateHandler); // Scroll to form document.getElementById('task-form').scrollIntoView({ behavior: 'smooth' }); } resetForm() { document.getElementById('task-form').reset(); } showMessage(message, type) { // Create message element const messageEl = document.createElement('div'); messageEl.className = `message ${type}`; messageEl.textContent = message; // Style the message messageEl.style.padding = '1rem'; messageEl.style.marginBottom = '1rem'; messageEl.style.borderRadius = 'var(--border-radius)'; if (type === 'success') { messageEl.style.backgroundColor = '#d4edda'; messageEl.style.color = '#155724'; messageEl.style.border = '1px solid #c3e6cb'; } else if (type === 'error') { messageEl.style.backgroundColor = '#f8d7da'; messageEl.style.color = '#721c24'; messageEl.style.border = '1px solid #f5c6cb'; } // Insert message at the top of main const main = document.querySelector('main'); main.insertBefore(messageEl, main.firstChild); // Remove message after 3 seconds setTimeout(() => { messageEl.remove(); }, 3000); } saveTasks() { localStorage.setItem('tasks', JSON.stringify(this.tasks)); } loadTasks() { const tasksJSON = localStorage.getItem('tasks'); if (tasksJSON) { try { const tasksData = JSON.parse(tasksJSON); return tasksData.map(taskData => Task.fromJSON(taskData)); } catch (error) { console.error('Error loading tasks:', error); return []; } } return []; } } // Initialize the app document.addEventListener('DOMContentLoaded', () => { new TaskManager(); });
Project Features
  • CRUD Operations: Create, Read, Update, and Delete tasks
  • Filtering: Filter tasks by status and priority
  • Sorting: Sort tasks by due date, priority, or title
  • Local Storage: Persist tasks in local storage
  • Responsive Design: Works on different screen sizes
  • Form Validation: Validate user input
  • User Feedback: Show success and error messages
  • Modern JavaScript: Uses ES6+ features like classes, arrow functions, and modules
Project Extensions

Once you've implemented the basic task manager, consider these extensions:

  • Add user authentication and authorization
  • Implement drag-and-drop to reorder tasks
  • Add categories or tags to tasks
  • Implement a search functionality
  • Add a calendar view for tasks
  • Implement task reminders or notifications
  • Add data visualization for task statistics
  • Implement collaborative features for sharing tasks
Interactive Demo

Try the task manager:

Task manager will appear here...