JavaScript Documentation
Complete guide to JavaScript programming from basics to advanced
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>
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:
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:
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:
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:
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:
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:
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:
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.
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:
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:
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:
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:
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:
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:
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:
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:
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}` : ''}
`;
}
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: