Vanilla Javascript Book
Vanilla Javascript Book
Advanced
By Manus AI
Table of Contents
1. Introduction to JavaScript
2. JavaScript Basics
3. Control Structures
4. Functions
5. Arrays and Objects
1. Design Patterns
2. Modules and Bundlers
3. Promises and Async/Await
4. Modern JavaScript APIs
5. Optimization and Performance
6. Advanced Code Examples
Appendices
1. Foundation for All Frameworks: Every JavaScript framework (React, Vue, Angular,
etc.) is built on vanilla JavaScript. Understanding the core language makes learning
any framework significantly easier.
4. Long-Term Relevance: While frameworks come and go, the core language remains
relevant and continues to evolve in a backward-compatible way.
Chapters 1-5 cover the fundamentals of JavaScript, including basic syntax, variables,
data types, control structures, functions, arrays, and objects. These chapters assume no
prior programming knowledge.
Chapters 6-11 explore more complex topics such as DOM manipulation, events,
advanced objects, closures, AJAX, and practical intermediate-level code examples.
Chapters 12-17 delve into sophisticated JavaScript concepts including design patterns,
modules, promises, modern APIs, performance optimization, and advanced real-world
code examples.
• Clear explanations of concepts with analogies and visual aids where appropriate
• Numerous code examples that demonstrate practical applications
• Common pitfalls and how to avoid them
• Best practices for writing clean, efficient code
• Exercises to reinforce learning
• Summary of key points
4. Build projects using what you've learned. Real-world application is the best way to
solidify knowledge.
5. Use the appendices as reference material when you encounter specific challenges.
• Clarity over brevity: Concepts are explained thoroughly, even if it means using
more words.
• Practical over theoretical: While theory is important, the focus is on practical
application.
• Progressive complexity: The book starts simple and gradually introduces more
complex concepts.
• Real-world relevance: Examples and exercises reflect real-world scenarios you'll
encounter as a developer.
A Living Language
JavaScript continues to evolve with new features added regularly. This book covers
JavaScript up to the latest stable ECMAScript standard at the time of writing, including
modern features like arrow functions, destructuring, async/await, and more.
Let's Begin
Whether you're taking your first steps in programming or looking to master advanced
JavaScript concepts, this book aims to be your comprehensive guide. Let's embark on
this journey together, exploring the power and flexibility of vanilla JavaScript.
Turn the page to begin with Chapter 1: Introduction to JavaScript, where we'll explore
the history, evolution, and fundamental concepts of this versatile language.
Happy coding!
Chapter 1: Introduction to JavaScript
JavaScript was initially created to "make web pages alive" by allowing developers to add
dynamic content, control multimedia, animate images, and much more. Today,
JavaScript has evolved far beyond its original purpose and can be used for:
What makes JavaScript particularly powerful is that it's the only programming language
that browsers can run natively, making it the universal language of web development.
• 1995: Brendan Eich created JavaScript in just 10 days while working at Netscape
Communications. It was initially called "Mocha," then renamed to "LiveScript," and
finally "JavaScript" as a marketing move to capitalize on the popularity of Java
(despite having little relation to Java).
• 1996: Microsoft released JScript, their own version of JavaScript for Internet
Explorer.
• 2015: A major update, ECMAScript 2015 (ES6), was released with significant
enhancements to the language, including classes, modules, arrow functions,
promises, and much more.
• 2016-Present: Annual releases (ES2016, ES2017, etc.) continue to add new features
and improvements to the language.
This evolution has transformed JavaScript from a simple scripting language for basic
web interactivity into a robust programming language capable of powering complex
applications.
ECMAScript is the official specification that defines how the JavaScript language should
work. It's maintained by ECMA International through the TC39 committee, which decides
what features should be added to the language.
Think of ECMAScript as the blueprint and JavaScript as the house built from that
blueprint. Other implementations of ECMAScript have existed (like ActionScript used in
Flash), but JavaScript is by far the most widely used.
When you hear terms like "ES6" or "ES2015," these refer to specific versions of the
ECMAScript specification that JavaScript implements.
Basic Setup
That's it! Unlike many programming languages, you don't need to install compilers or
complex development environments to get started.
<!DOCTYPE html>
<html>
<head>
<title>My First JavaScript Page</title>
</head>
<body>
<h1>Hello, JavaScript!</h1>
As you progress, you might want to enhance your development environment with:
• Node.js: A JavaScript runtime that allows you to run JavaScript outside the
browser
• npm (Node Package Manager): For installing and managing JavaScript libraries and
tools
• ESLint: For code quality and style checking
• Prettier: For automatic code formatting
• Git: For version control
• Build tools: Like Webpack, Parcel, or Vite for modern JavaScript development
We'll explore these tools in later chapters as you advance in your JavaScript journey.
1.5 Developer Tools in Modern Browsers
Modern browsers come equipped with powerful developer tools that are essential for
JavaScript development. Let's explore the Chrome DevTools as an example (other
browsers have similar tools):
1. Console Panel:
2. Run JavaScript code directly
3. View errors, warnings, and log messages
4. Test ideas and debug code
1. Elements Panel:
2. Inspect and modify the HTML and CSS of a page
4. Sources Panel:
8. Network Panel:
These tools will become your best friends as you develop JavaScript applications. We'll
use them extensively throughout this book to understand how our code works and to
troubleshoot issues.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<title>My First JavaScript Program</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin-top: 50px;
}
#message {
color: blue;
font-size: 24px;
margin: 20px;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Hello, JavaScript!</h1>
<div id="message">Welcome to JavaScript Programming</div>
<button id="changeButton">Change Message</button>
Create a file named script.js in the same folder with the following content:
2. document.addEventListener("DOMContentLoaded", function()
{ ... }); - This sets up a function to run when the HTML document has been
completely loaded.
Summary
• What JavaScript is: A versatile programming language primarily used for web
development
• The history of JavaScript: From its creation in 1995 to modern ECMAScript
standards
• JavaScript vs ECMAScript: The relationship between the specification and the
language
• Setting up a development environment: The tools you need to start coding
• Browser developer tools: Essential tools for JavaScript development
• Your first JavaScript program: A simple interactive webpage
JavaScript is the language that powers interactivity on the web. It allows you to create
dynamic, responsive user experiences that go far beyond what's possible with HTML and
CSS alone. As we progress through this book, you'll learn how to harness the full power
of JavaScript to build increasingly sophisticated applications.
Exercises
Open your browser's developer tools and experiment with the console: 1. Try basic
arithmetic: 5 + 10 , 20 * 3 , etc. 2. Create a variable: let name = "Your Name" 3.
Use that variable: console.log("Hello, " + name) 4. Try a simple function:
function greet() { return "Hello, world!"; } 5. Call your function:
greet()
Take the program we created and make these changes: 1. Add another button that
changes the message back to the original text 2. Make the message change to a random
color when clicked 3. Add a counter that tracks how many times the button has been
clicked
Create a new HTML and JavaScript file pair that: 1. Displays the current date and time on
the page 2. Has a button that, when clicked, updates the date and time 3. Changes the
background color of the page based on the time of day (morning, afternoon, evening)
Exercise 4: Research
Use the internet to research and answer these questions: 1. What are three major
differences between JavaScript and Java? 2. What new features were introduced in ES6
(ECMAScript 2015)? 3. What are some popular JavaScript frameworks and libraries used
today?
By completing these exercises, you'll gain hands-on experience with JavaScript basics
and be well-prepared for the next chapter, where we'll dive deeper into JavaScript's core
concepts.
Let
The let keyword was introduced in ES6 (ECMAScript 2015) and is now the preferred
way to declare variables in JavaScript:
1. Block scope: Variables declared with let are limited to the block, statement, or
expression where they are defined.
if (true) {
let blockScoped = "I exist only in this block";
console.log(blockScoped); // "I exist only in this block"
}
console.log(blockScoped); // Error: blockScoped is not defined
1. Reassignment: You can change the value of a variable declared with let .
let count = 1;
count = 2; // This is allowed
1. No redeclaration: You cannot declare the same variable twice in the same scope.
let x = 10;
let x = 20; // Error: Identifier 'x' has already been declared
1. No hoisting: Variables declared with let are not hoisted to the top of their scope.
Const
The const keyword also came with ES6 and is used to declare constants - variables
whose values cannot be changed:
const PI = 3.14159;
const MAX_USERS = 100;
const API_KEY = "abc123xyz";
1. Reference mutability: For complex types like objects and arrays, the content can
be modified even though the reference cannot change.
Var (Legacy)
Before ES6, var was the only way to declare variables in JavaScript:
1. Function scope: Variables declared with var are scoped to the function in which
they are defined, or global if declared outside any function.
function example() {
var functionScoped = "I exist in the entire function";
if (true) {
var alsoFunctionScoped = "I also exist in the entire
function";
}
console.log(functionScoped); // "I exist in the entire
function"
console.log(alsoFunctionScoped); // "I also exist in the
entire function"
}
1. Hoisting: Variables declared with var are hoisted to the top of their scope.
1. Redeclaration allowed: You can declare the same variable multiple times.
var y = 1;
var y = 2; // This is allowed
Good variable names make your code more readable and maintainable:
4. Descriptive names: Choose names that explain what the variable contains
```javascript // Bad let x = 86400000;
1. Number
2. String
The string type represents text data, enclosed in single quotes, double quotes, or
backticks:
String operations:
// Concatenation
let greeting = "Hello" + " " + "World"; // "Hello World"
// String length
let length = "JavaScript".length; // 10
// Accessing characters
let firstChar = "JavaScript"[0]; // "J"
let lastChar = "JavaScript"[9]; // "t"
// String methods
let upperCase = "javascript".toUpperCase(); // "JAVASCRIPT"
let substring = "JavaScript".substring(0, 4); // "Java"
let replaced = "Hello World".replace("World", "JavaScript"); //
"Hello JavaScript"
Template literals (using backticks) allow for embedded expressions and multi-line
strings:
// String interpolation
let message = `${name} is ${age} years old`;
// Multi-line string
let multiLine = `This is line one
This is line two
This is line three`;
3. Boolean
The boolean type has only two values: true and false :
4. Undefined
A variable that has been declared but not assigned a value has the value undefined :
let notDefined;
console.log(notDefined); // undefined
function noReturn() {
// No return statement
}
console.log(noReturn()); // undefined
5. Null
The null value represents the intentional absence of any object value:
Note the difference between null and undefined : - undefined means a variable
has been declared but not assigned a value - null is an assignment value that
represents "nothing" or "empty"
6. Symbol (ES6)
Symbols are unique and immutable primitive values, often used as object property keys:
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // Using the symbol as a property key
};
console.log(user[id]); // 123
7. BigInt (ES2020)
BigInt allows you to work with integers larger than the Number type can represent:
1. Object
let person = {
firstName: "John",
lastName: "Doe",
age: 30,
isEmployed: true,
skills: ["JavaScript", "HTML", "CSS"],
address: {
street: "123 Main St",
city: "Boston",
country: "USA"
},
greet: function() {
return `Hello, my name is ${this.firstName}`;
}
};
// Bracket notation
console.log(person["lastName"]); // "Doe"
let property = "age";
console.log(person[property]); // 30
// Calling a method
console.log(person.greet()); // "Hello, my name is John"
// Deleting a property
delete person.isEmployed;
2. Array
console.log(fruits[0]); // "Apple"
console.log(fruits[2]); // "Cherry"
console.log(fruits.length); // 3
// Adding elements
fruits.push("Date"); // Adds to the end: ["Apple", "Banana",
"Cherry", "Date"]
fruits.unshift("Apricot"); // Adds to the beginning: ["Apricot",
"Apple", "Banana", "Cherry", "Date"]
// Removing elements
let last = fruits.pop(); // Removes from the end: last = "Date"
let first =
fruits.shift(); // Removes from the beginning: first = "Apricot"
// Finding elements
let index = fruits.indexOf("Banana"); // 1
let includes = fruits.includes("Cherry"); // true
// Transforming arrays
let upperFruits = fruits.map(fruit => fruit.toUpperCase()); //
["APPLE", "BANANA", "CHERRY"]
let longFruits = fruits.filter(fruit => fruit.length > 5); //
["Banana", "Cherry"]
let joinedFruits = fruits.join(", "); // "Apple, Banana, Cherry"
// Iterating
fruits.forEach(fruit => console.log(fruit));
3. Function
// Function declaration
function add(a, b) {
return a + b;
}
// Function expression
const multiply = function(a, b) {
return a * b;
};
// Calling functions
console.log(add(5, 3)); // 8
console.log(multiply(4, 2)); // 8
console.log(divide(10, 2)); // 5
Type Checking
To check the type of a value, you can use the typeof operator:
2.3 Operators
Operators allow you to perform operations on variables and values.
let a = 10;
let b = 3;
// Basic operations
let sum = a + b; // 13
let difference = a - b; // 7
let product = a * b; // 30
let quotient = a / b; // 3.3333...
let remainder = a % b; // 1 (modulus/remainder)
let exponent = a ** b; // 1000 (10^3, exponentiation, ES2016)
// Compound assignment
x += 5; // x = x + 5 (15)
x -= 3; // x = x - 3 (12)
x *= 2; // x = x * 2 (24)
x /= 4; // x = x / 4 (6)
x %= 4; // x = x % 4 (2)
x **= 3; // x = x ** 3 (8)
// Multiple assignment
let a, b, c;
a = b = c = 5; // All variables are assigned the value 5
let a = 5;
let b = 10;
let c = "5";
// Equality
console.log(a == c); // true (loose equality, converts types)
console.log(a === c); // false (strict equality, checks types)
console.log(a != b); // true (loose inequality)
console.log(a !== c); // true (strict inequality)
// Relational
console.log(a < b); // true
console.log(a > b); // false
console.log(a <= b); // true
console.log(a >= b); // false
// Comparing objects
let obj1 = { name: "John" };
let obj2 = { name: "John" };
let obj3 = obj1;
// OR operator (||)
console.log(x > 0 || y > 20); // true (first condition is true)
console.log(x > 10 || y > 20); // false (both conditions are
false)
Short-circuit evaluation:
// && returns the first falsy value or the last value if all are
truthy
console.log(0 && "Hello"); // 0 (first falsy value)
console.log("Hello" && "World"); // "World" (last value, all
truthy)
// Nested ternary
let result = age < 13 ? "Child" : age < 18 ? "Teenager" :
"Adult";
console.log(result); // "Adult"
// Combining arrays
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// Copying arrays
let original = [1, 2, 3];
let copy = [...original]; // [1, 2, 3]
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
let user = {
profile: {
// address is missing
}
};
// Without optional chaining
// let city = user.profile.address.city; // Error: Cannot read
property 'city' of undefined
Destructuring Assignment
// Array destructuring
let [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1 2 3
// Object destructuring
let person = { name: "Alice", age: 25, job: "Developer" };
let { name, age } = person;
console.log(name, age); // "Alice" 25
// Default values
let [x = 0, y = 0, z = 0] = [5, 10];
console.log(x, y, z); // 5 10 0
// Renaming properties
let { name: fullName, job: profession } = person;
console.log(fullName, profession); // "Alice" "Developer"
String Conversion
Number Conversion
// Using + operator
let num3 = +"123"; // 123
Boolean Conversion
String Conversion
Boolean Conversion
if (0) {
console.log("This won't run because 0 is falsy");
}
In JavaScript, values are considered either "truthy" or "falsy" when used in a boolean
context:
Falsy Values
The following values are always falsy: - false - 0 (zero) - "" (empty string) - null -
undefined - NaN (Not a Number)
Truthy Values
Everything else is truthy, including: - true - Any number other than 0 - Any non-empty
string - All objects and arrays (even empty ones) - All functions
// Examples
if ([]) console.log("Empty array is truthy");
if ({}) console.log("Empty object is truthy");
if ("0") console.log("String with zero is truthy");
if (new Boolean(false)) console.log("Boolean object is truthy");
2.5 Template Literals
Template literals (introduced in ES6) provide an improved way to work with strings:
Basic Syntax
String Interpolation
let a = 5;
let b = 10;
let sum = `${a} + ${b} = ${a + b}`; // "5 + 10 = 15"
Multi-line Strings
console.log(multiLine);
// Output:
// This is line one
// This is line two
// This is line three
Tagged Templates
Summary
• Variables and Constants: How to declare and use variables with let , const ,
and var
• Data Types: JavaScript's primitive types (number, string, boolean, undefined, null,
symbol, bigint) and complex types (object, array, function)
• Operators: Arithmetic, assignment, comparison, logical, and other important
operators
• Type Conversion and Coercion: How JavaScript handles type conversions, both
explicitly and implicitly
• Template Literals: Modern string handling with interpolation and multi-line
support
1. Declare variables using let , const , and var and observe their behavior in
different scopes.
2. Create variables for each primitive type and use typeof to verify their types.
3. Create an object that contains properties of different data types, including a nested
object and an array.
Exercise 3: Operators
1. Write expressions using all arithmetic operators and verify the results.
2. Create examples of short-circuit evaluation with && and || operators.
3. Use the nullish coalescing operator ( ?? ) to provide default values for potentially
null or undefined variables.
4. Use the optional chaining operator ( ?. ) to safely access nested properties in an
object.
1. Create a template literal that includes variables, expressions, and function calls.
2. Write a multi-line template literal that formats a user profile.
3. Create a tagged template function that formats numbers as currency.
Create a simple calculator script that: 1. Declares variables for two numbers and an
operation ('+', '-', '*', '/') 2. Uses a switch statement to perform the appropriate
calculation 3. Uses template literals to display the calculation and result 4. Handles
potential errors (like division by zero) 5. Converts the result to different types and
displays them
By completing these exercises, you'll gain practical experience with JavaScript's basic
concepts and be well-prepared for the next chapter on control structures.
Chapter 3: Control Structures
Control structures are the backbone of programming logic, allowing you to control the
flow of your program's execution. They help you make decisions, repeat actions, and
organize your code in a logical manner. In this chapter, we'll explore JavaScript's control
structures in detail.
The if statement is the most fundamental control structure. It executes a block of code
if a specified condition is true.
Basic if Statement
if (condition) {
// Code to execute if condition is true
}
Example:
if/else Statement
The if/else statement provides an alternative block of code to execute when the
condition is false.
if (condition) {
// Code to execute if condition is true
} else {
// Code to execute if condition is false
}
Example:
For multiple conditions, you can use else if to check additional conditions after the
first one.
if (condition1) {
// Code to execute if condition1 is true
} else if (condition2) {
// Code to execute if condition1 is false and condition2 is
true
} else {
// Code to execute if all conditions are false
}
Example:
Nested if Statements
You can also nest if statements inside other if statements for more complex
conditions.
if (condition1) {
if (condition2) {
// Code to execute if both condition1 and condition2 are
true
} else {
// Code to execute if condition1 is true but condition2
is false
}
} else {
// Code to execute if condition1 is false
}
Example:
1. Keep conditions simple: Break complex conditions into smaller, more readable
parts.
// Instead of this
if (age > 18 && (status === 'student' || status ===
'unemployed') && !isDisabled) {
// Code
}
// Do this
let isAdult = age > 18;
let isEligibleStatus = status === 'student' || status ===
'unemployed';
let isQualified = isAdult && isEligibleStatus && !isDisabled;
if (isQualified) {
// Code
}
1. Use curly braces consistently: Even for single-line blocks, use curly braces for
clarity and to prevent bugs.
// Not recommended
if (isRaining) console.log("Take an umbrella!");
// Recommended
if (isRaining) {
console.log("Take an umbrella!");
}
// Clear intention
x = 10;
if (x === 10) {
// Code
}
The switch statement provides a way to handle multiple conditions that all compare
the same value. It's often cleaner than multiple if/else if statements when you're
checking a single variable against different values.
Basic Syntax
switch (expression) {
case value1:
// Code to execute if expression === value1
break;
case value2:
// Code to execute if expression === value2
break;
// More cases...
default:
// Code to execute if no case matches
}
The break statement is crucial - without it, execution would "fall through" to the next
case, which is rarely what you want.
Example:
switch (day) {
case 0:
console.log("It's Sunday!");
break;
case 1:
console.log("It's Monday!");
break;
case 2:
console.log("It's Tuesday!");
break;
case 3:
console.log("It's Wednesday!");
break;
case 4:
console.log("It's Thursday!");
break;
case 5:
console.log("It's Friday!");
break;
case 6:
console.log("It's Saturday!");
break;
default:
console.log("Something went wrong with the date!");
}
Fall-Through Behavior
Sometimes, you might want multiple cases to execute the same code. In this case, you
can omit the break statement to create a "fall-through" effect:
switch (day) {
case 0:
case 6:
typeOfDay = "Weekend";
break;
case 1:
case 2:
case 3:
case 4:
case 5:
typeOfDay = "Weekday";
break;
default:
typeOfDay = "Unknown";
}
console.log(`Today is a ${typeOfDay}.`);
• Use switch when comparing a single variable against multiple discrete values
• Use if/else for complex conditions, range checks, or when comparing different
variables
The ternary (or conditional) operator provides a shorthand way to write simple if/
else statements. It's particularly useful for simple conditional assignments.
Syntax
Example:
For better readability, you can format nested ternaries with line breaks:
1. Use for simple conditions: Ternary operators are best for simple, straightforward
conditions.
2. Avoid deep nesting: More than one level of nesting can quickly become hard to
read.
3. Use parentheses for clarity: When combining with other operators, use
parentheses to make the order of operations clear.
3.2 Loops
Loops allow you to execute a block of code repeatedly. JavaScript provides several types
of loops, each with its own use cases.
The for loop is one of the most common loops in JavaScript. It's ideal when you know
in advance how many times you want to execute a block of code.
Syntax
Example:
// Count from 1 to 5
for (let i = 1; i <= 5; i++) {
console.log(i);
}
// Output: 1, 2, 3, 4, 5
You can include multiple expressions in the initialization and update parts of a for loop
by separating them with commas:
Optional Parts
The while loop executes a block of code as long as a specified condition is true. It's
useful when you don't know in advance how many iterations you need.
Syntax
while (condition) {
// Code to execute in each iteration
}
Example:
let count = 1;
function findFactors(num) {
let factor = 2;
let factors = [];
return factors;
}
console.log(findFactors(60)); // [2, 2, 3, 5]
The do...while loop is similar to the while loop, but it always executes the code
block at least once before checking the condition.
Syntax
do {
// Code to execute in each iteration
} while (condition);
Example:
let count = 1;
do {
console.log(count);
count++;
} while (count <= 5);
// Output: 1, 2, 3, 4, 5
The do...while loop is useful when you want to ensure that the code block executes
at least once, regardless of the condition:
let answer;
do {
// In a browser environment, you might use prompt()
// answer = prompt("Please enter a number greater than
10:");
The for...in loop iterates over all enumerable properties of an object. It's primarily
designed for objects, not arrays.
Syntax
let person = {
firstName: "John",
lastName: "Doe",
age: 30,
occupation: "Developer"
};
While you can use for...in with arrays, it's generally not recommended because:
The for...of loop (introduced in ES6) iterates over iterable objects like arrays, strings,
maps, sets, etc. Unlike for...in , it accesses the values directly, not the property
names.
Syntax
Arrays also have a forEach method, which is similar to for...of but with some
differences:
// Using for...of
for (let num of numbers) {
console.log(num);
}
// Using forEach
numbers.forEach(function(num) {
console.log(num);
});
Key differences: - forEach is a method, not a loop statement - forEach provides the
index and array as additional parameters - You can't use break or continue with
forEach - forEach is slightly slower than for...of
The break statement terminates the current loop or switch statement and transfers
control to the statement following the terminated statement.
for (let i = 1; i <= 10; i++) {
if (i === 5) {
break; // Exit the loop when i is 5
}
console.log(i);
}
// Output: 1, 2, 3, 4
The continue statement skips the current iteration of a loop and continues with the
next iteration.
Labeled Statements
Labels can be used with break and continue to specify which loop to break from or
continue in nested loops:
With continue :
Summary
• Conditional Statements:
• if/else statements for basic decision making
• switch statements for comparing a single value against multiple cases
• Loops:
• Control Flow:
Exercises
1. Write a function that takes a temperature value and returns "Hot" if it's above 30,
"Warm" if it's between 20 and 30, "Cool" if it's between 10 and 20, and "Cold" if it's
below 10.
3. Write a switch statement that takes a month number (1-12) and returns the
number of days in that month. Consider leap years for February (assume the
current year is a leap year).
Exercise 2: Loops
1. Write a function that uses a for loop to calculate the sum of all numbers from 1 to
n, where n is a parameter.
2. Create a function that uses a while loop to find the first power of 2 that is greater
than 1000.
3. Write a program that uses a do...while loop to ask the user for a number
between 1 and 10 until they enter a valid number. (You can simulate user input for
this exercise.)
1. Write a program that prints a multiplication table for numbers 1 through 10 using
nested for loops.
2. Create a function that checks if a number is prime using a loop and conditional
statements.
3. Write a program that uses nested loops and the break statement to find the first
pair of numbers (i, j) where i * j is greater than 100 and both i and j are less than 15.
Exercise 4: Practical Application
Create a simple number guessing game that: 1. Generates a random number between 1
and 100 2. Asks the user to guess the number (simulate user input) 3. Tells the user if
their guess is too high, too low, or correct 4. Keeps track of the number of guesses 5.
Ends when the user guesses correctly or reaches a maximum number of attempts 6. Uses
appropriate control structures (loops and conditionals)
By completing these exercises, you'll gain practical experience with JavaScript's control
structures and be well-prepared for the next chapter on functions.
Chapter 4: Functions
Functions are one of the fundamental building blocks in JavaScript. They allow you to
encapsulate a piece of code that performs a specific task, making your code more
organized, reusable, and maintainable. In this chapter, we'll explore functions in depth,
from basic declarations to advanced concepts.
Basic Syntax
Example:
function greet(name) {
return `Hello, ${name}!`;
}
One important characteristic of function declarations is that they are hoisted to the top
of their scope. This means you can call a function before it's declared in your code:
// Function declaration
function add(a, b) {
return a + b;
}
This works because during the creation phase of the JavaScript engine's execution
context, function declarations are moved to the top of their scope.
Functions don't always need to return a value. When a function doesn't have a return
statement, or has an empty return statement, it implicitly returns undefined :
function logMessage(message) {
console.log(message);
// No return statement
}
A function can have multiple return statements, but only one will be executed:
function getAbsoluteValue(number) {
if (number >= 0) {
return number;
} else {
return -number;
}
}
console.log(getAbsoluteValue(5)); // 5
console.log(getAbsoluteValue(-5)); // 5
Early Return Pattern
The early return pattern is a common technique to make functions more readable by
handling edge cases or error conditions early:
function divide(a, b) {
// Handle the edge case early
if (b === 0) {
return "Cannot divide by zero";
}
// Main logic
return a / b;
}
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // "Cannot divide by zero"
Basic Syntax
Example:
console.log(multiply(4, 5)); // 20
Function expressions don't require a name. When a function doesn't have a name, it's
called an anonymous function:
const square = function(number) {
return number * number;
};
console.log(square(8)); // 64
console.log(factorial(5)); // 120
The name calculateFactorial is only accessible within the function itself, which is
useful for recursion. Outside the function, you use the variable name factorial .
// This works
console.log(add(2, 3)); // 5
function add(a, b) {
return a + b;
}
Basic Syntax
Example:
console.log(add(5, 3)); // 8
Concise Body Syntax
If the function body consists of a single expression, you can omit the curly braces and the
return keyword:
Parameter Handling
Object Literals
When returning an object literal directly, wrap it in parentheses to avoid confusion with
the function body:
console.log(createPerson("Alice",
30)); // { name: "Alice", age: 30 }
Key Differences from Regular Functions
1. No this binding: Arrow functions don't have their own this context; they
inherit this from the surrounding code.
const person = {
name: "Alice",
regularFunction: function() {
console.log(this.name); // "Alice"
},
arrowFunction: () => {
console.log(this.name); // undefined (or global name if
defined)
}
};
person.regularFunction(); // "Alice"
person.arrowFunction(); // undefined
1. No arguments object: Arrow functions don't have their own arguments object.
function regularFunction() {
console.log(arguments);
}
1. Cannot be used as constructors: Arrow functions cannot be used with the new
keyword.
function RegularFunction() {
this.value = 42;
}
2. Cannot be used as methods: Due to the this binding behavior, arrow functions
are not suitable as object methods when you need to access the object via this .
Missing Arguments
If you call a function with fewer arguments than parameters, the missing parameters are
set to undefined :
Extra Arguments
If you call a function with more arguments than parameters, the extra arguments are
ignored (but available through the arguments object):
function add(a, b) {
console.log(arguments); // [1, 2, 3, 4]
return a + b;
}
The arguments object is an array-like object that contains all 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, 4, 5)); // 15
Basic Syntax
Example:
return {
name,
role,
permissions
};
}
console.log(createUser("Alice", "admin"));
// { name: "Alice", role: "admin", permissions: ["read",
"write", "delete"] }
console.log(createUser("Bob"));
// { name: "Bob", role: "user", permissions: ["read"] }
Basic Syntax
Example:
1. Rest parameters are a real array, so you can use array methods directly:
function sum(...numbers) {
// Using array method reduce
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
1. Rest parameters only include the arguments that don't have corresponding
parameter names:
example(1, 2, 3, 4, 5);
1. Rest parameters work with arrow functions, unlike the arguments object:
const sum = (...numbers) => numbers.reduce((total, num) =>
total + num, 0);
console.log(sum(1, 2, 3, 4, 5)); // 15
Restrictions
// Valid
function example(a, b, ...rest) { }
Basic Return
function add(a, b) {
return a + b;
}
JavaScript functions can only return a single value. However, you can return multiple
values by using an array or an object:
// Using an array
function getCoordinates() {
const x = 10;
const y = 20;
return [x, y];
}
// Using an object
function getUserInfo() {
return {
name: "Alice",
age: 30,
isAdmin: false
};
}
Conditional Returns
console.log(getDiscount(100, "premium")); // 20
console.log(getDiscount(100, "standard")); // 10
console.log(getDiscount(100, "none")); // 0
For arrow functions with a concise body (no curly braces), the expression is implicitly
returned:
// Equivalent to:
const addVerbose = (a, b) => {
return a + b;
};
4.8 Function Scope
Scope determines the accessibility of variables and functions in your code. JavaScript
has three types of scope:
1. Global scope
2. Function scope
3. Block scope (introduced in ES6 with let and const )
Variables declared inside a function are only accessible within that function:
function example() {
const localVar = "I'm local";
console.log(localVar); // "I'm local"
}
example();
// console.log(localVar); // Error: localVar is not defined
function outer() {
const outerVar = "I'm from outer";
function inner() {
const innerVar = "I'm from inner";
console.log(outerVar); // "I'm from outer" - accessible
from parent scope
console.log(innerVar); // "I'm from inner"
}
inner();
// console.log(innerVar); // Error: innerVar is not defined
}
outer();
Lexical Scope
JavaScript uses lexical (static) scoping, which means the scope of a variable is
determined by its location in the source code:
const globalVar = "I'm global";
function outer() {
const outerVar = "I'm from outer";
function inner() {
const innerVar = "I'm from inner";
console.log(globalVar); // "I'm global"
console.log(outerVar); // "I'm from outer"
console.log(innerVar); // "I'm from inner"
}
return inner;
}
This leads us to the concept of closures, which we'll explore in more detail in a later
chapter.
Variable Shadowing
When a variable in an inner scope has the same name as a variable in an outer scope, the
inner variable "shadows" the outer one:
function example() {
const value = "local";
console.log(value); // "local"
}
example();
console.log(value); // "global"
1. Minimize global variables: Global variables can be accessed and modified from
anywhere, which can lead to bugs and unintended side effects.
2. Use block scope with let and const: Prefer let and const over var for more
predictable scoping behavior.
3. Keep functions focused: Functions should do one thing and do it well. This often
means keeping them small and focused.
4. Avoid modifying variables from outer scopes: This can make your code harder to
understand and debug.
Summary
Exercises
4. Create a function that calculates the area of a circle (π × radius²). Use Math.PI
for the value of π.
1. Create a function that accepts any number of arguments and returns their sum.
2. Write a function that accepts an array as its first parameter and a callback function
as its second parameter. The function should apply the callback to each element of
the array and return a new array with the results.
1. Write a function that returns the maximum value from an array of numbers.
2. Create a function that returns multiple values using an object (e.g., both the area
and perimeter of a rectangle).
1. Create a nested function where the inner function uses a variable from the outer
function.
1. Create an object called calculator with methods for basic operations (add,
subtract, multiply, divide).
By completing these exercises, you'll gain practical experience with JavaScript functions
and be well-prepared for the next chapter on arrays and objects.
Chapter 5: Arrays and Objects
Arrays and objects are fundamental data structures in JavaScript that allow you to store
and manipulate collections of data. While we briefly introduced them in Chapter 2, this
chapter will explore them in much greater depth, showing you how to leverage their full
power in your applications.
5.1 Arrays
Arrays are ordered collections of values that can be of any type. They are incredibly
versatile and come with many built-in methods that make data manipulation easier.
// Empty array
let emptyArray = [];
Array Constructor
// Empty array
let emptyArray = new Array();
The Array.of() method creates a new array with the provided arguments as
elements, regardless of the number or types of arguments:
The Array.from() method creates a new array from an array-like or iterable object:
// From a string
let chars = Array.from("Hello");
console.log(chars); // ["H", "e", "l", "l", "o"]
// From a Set
let uniqueNumbers = Array.from(new Set([1, 2, 2, 3, 3, 3]));
console.log(uniqueNumbers); // [1, 2, 3]
You can access array elements using their index (position), which starts at 0:
// Accessing by index
console.log(fruits[0]); // "Apple"
console.log(fruits[2]); // "Cherry"
The at() method allows you to use negative indices to access elements from the end of
the array:
If you try to access an element at an index that doesn't exist, JavaScript returns
undefined :
JavaScript arrays come with many built-in methods that make them powerful and
flexible.
// push() adds elements to the end and returns the new length
let newLength = fruits.push("Cherry", "Date");
console.log(newLength); // 4
console.log(fruits); // ["Apple", "Banana", "Cherry", "Date"]
Modifying Arrays
Changing elements:
Splicing arrays:
// Remove elements
// splice(start, deleteCount, ...items)
let removed = fruits.splice(1,
2); // Remove 2 elements starting at index 1
console.log(removed); // ["Banana", "Cherry"]
console.log(fruits); // ["Apple", "Date"]
Slicing arrays:
The slice() method returns a shallow copy of a portion of an array without modifying
the original array:
let people = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charlie", age: 35 },
{ name: "David", age: 30 }
];
// filter() creates a new array with all elements that pass the
test
let thirtyYearOlds = people.filter(person => person.age === 30);
console.log(thirtyYearOlds); // [{ name: "Bob", age: 30 },
{ name: "David", age: 30 }]
Transforming Arrays
Mapping:
Reducing:
// Counting occurrences
let fruits = ["apple", "banana", "apple", "orange", "banana",
"apple"];
let count = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(count); // { apple: 3, banana: 2, orange: 1 }
Flattening:
Sorting arrays:
// Sorting objects
let people = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charlie", age: 20 }
];
Reversing arrays:
Joining arrays:
let fruits = ["Apple", "Banana", "Cherry"];
Filling arrays:
Testing arrays:
for Loop
forEach Method
The forEach method executes a provided function once for each array element:
Destructuring allows you to extract values from arrays into distinct variables:
// Basic destructuring
let [x, y, z] = coordinates;
console.log(x, y, z); // 10 20 30
// Skip elements
let [first, , third] = ["Apple", "Banana", "Cherry"];
console.log(first, third); // "Apple" "Cherry"
// Rest pattern
let [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
// Default values
let [a = 1, b = 2, c = 3] = [10, 20];
console.log(a, b, c); // 10 20 3
// Swapping variables
let m = 1, n = 2;
[m, n] = [n, m];
console.log(m, n); // 2 1
Typed arrays are array-like objects that provide a mechanism for accessing raw binary
data:
5.2 Objects
Objects in JavaScript are collections of key-value pairs, where keys are strings (or
Symbols) and values can be any data type, including other objects.
// Empty object
let emptyObject = {};
Object Constructor
The Object.create() method creates a new object with the specified prototype
object:
let personProto = {
greet: function() {
return `Hello, my name is ${this.firstName} $
{this.lastName}`;
}
};
Dot Notation
let person = {
firstName: "John",
lastName: "Doe",
age: 30
};
console.log(person.firstName); // "John"
console.log(person.age); // 30
Bracket Notation
let person = {
firstName: "John",
lastName: "Doe",
age: 30,
"full-name": "John Doe" // Property with special characters
};
console.log(person["firstName"]); // "John"
console.log(person["full-name"]); // "John Doe"
• Use dot notation when you know the property name in advance and it's a valid
identifier
• Use bracket notation when:
• The property name contains special characters or spaces
• The property name is stored in a variable
• The property name is determined at runtime
let person = {
firstName: "John",
lastName: "Doe",
// Method
fullName: function() {
return this.firstName + " " + this.lastName;
},
In object methods, this refers to the object the method belongs to:
let person = {
name: "John",
sayName() {
console.log(this.name);
}
};
person.sayName(); // "John"
let person = {
name: "John",
for...in Loop
let person = {
firstName: "John",
lastName: "Doe",
age: 30
};
// Or with for...of
for (let [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`);
}
Object Manipulation
let person = {
firstName: "John",
lastName: "Doe"
};
console.log(person);
// { firstName: "Jane", lastName: "Smith", age: 30, email:
"[email protected]" }
Deleting Properties
let person = {
firstName: "John",
lastName: "Doe",
age: 30,
email: "[email protected]"
};
// Delete a property
delete person.age;
delete person["email"];
let person = {
firstName: "John",
lastName: "Doe",
age: 30
};
// Using in operator
console.log("firstName" in person); // true
console.log("email" in person); // false
// Using hasOwnProperty method (only checks own properties, not
inherited)
console.log(person.hasOwnProperty("firstName")); // true
console.log(person.hasOwnProperty("toString")); // false
(inherited from Object.prototype)
Object.assign() (ES6)
The Object.assign() method copies all enumerable own properties from one or
more source objects to a target object:
let person = {
firstName: "John",
lastName: "Doe"
};
let details = {
age: 30,
occupation: "Developer"
};
let address = {
city: "Boston",
country: "USA"
};
// Merge objects
let completePerson = Object.assign({}, person, details,
address);
console.log(completePerson);
// {
// firstName: "John",
// lastName: "Doe",
// age: 30,
// occupation: "Developer",
// city: "Boston",
// country: "USA"
// }
// Clone an object
let personClone = Object.assign({}, person);
// Update multiple properties
Object.assign(person, { firstName: "Jane", age: 25 });
console.log(person); // { firstName: "Jane", lastName: "Doe",
age: 25 }
let person = {
firstName: "John",
lastName: "Doe",
age: 30
};
Object.seal(user);
user.password = "newpassword"; // Works
user.email = "[email protected]"; // Silently fails
delete user.username; // Silently fails
console.log(user); // { username: "johndoe", password:
"newpassword" }
console.log(Object.isSealed(user)); // true
Destructuring allows you to extract values from objects into distinct variables:
let person = {
firstName: "John",
lastName: "Doe",
age: 30,
address: {
city: "Boston",
country: "USA"
}
};
// Basic destructuring
let { firstName, lastName } = person;
console.log(firstName, lastName); // "John" "Doe"
// Default values
let { age, occupation = "Unknown" } = person;
console.log(age, occupation); // 30 "Unknown"
// Nested destructuring
let { address: { city, country } } = person;
console.log(city, country); // "Boston" "USA"
// Rest pattern
let { firstName: name, ...rest } = person;
console.log(name); // "John"
console.log(rest); // { lastName: "Doe", age: 30, address:
{ city: "Boston", country: "USA" } }
let person = {
[propName]: "John", // Computed property name
["last" + "Name"]: "Doe",
[`age_${new Date().getFullYear() - 1990}`]: 30
};
When a property name is the same as the variable name, you can use the shorthand
syntax:
let firstName = "John";
let lastName = "Doe";
let age = 30;
// Instead of
let person1 = {
firstName: firstName,
lastName: lastName,
age: age
};
5.3 JSON
JSON (JavaScript Object Notation) is a lightweight data interchange format that is easy
for humans to read and write and easy for machines to parse and generate. It's based on
a subset of JavaScript object notation.
JSON Syntax
JSON syntax is similar to JavaScript object literals, but with some restrictions: - Property
names must be double-quoted strings - Values can only be strings, numbers, objects,
arrays, booleans, or null - No functions, undefined, or comments are allowed
{
"firstName": "John",
"lastName": "Doe",
"age": 30,
"isEmployed": true,
"skills": ["JavaScript", "HTML", "CSS"],
"address": {
"street": "123 Main St",
"city": "Boston",
"country": "USA"
}
}
Converting Between JSON and JavaScript Objects
JSON.stringify()
let person = {
firstName: "John",
lastName: "Doe",
age: 30,
isEmployed: true,
skills: ["JavaScript", "HTML", "CSS"],
greet: function() { return "Hello"; } // This will be
ignored in JSON
};
JSON.parse()
console.log(person.firstName); // "John"
console.log(person.age); // 30
1. API Communication: Most web APIs send and receive data in JSON format
2. Configuration Files: Many applications use JSON for configuration
3. Data Storage: JSON is often used for storing data in files or databases
4. Cross-Origin Data Transfer: JSON with Padding (JSONP) and CORS enable cross-
origin requests
Summary
• Array destructuring
Arrays and objects are fundamental data structures in JavaScript that you'll use in
virtually every application you build. Understanding how to work with them effectively is
crucial for becoming a proficient JavaScript developer.
Exercises
Exercise 1: Arrays
7. Create a new array with only books that have titles longer than 10 characters
8. Write a function that takes an array of numbers and returns a new array with only
the even numbers.
9. Create a function that flattens a nested array (without using the built-in flat()
method).
Exercise 2: Objects
1. Create an object representing a car with properties for make, model, year, and
color.
2. Add a method to the car object that returns the car's age (current year - car's year).
3. Create a function that takes two car objects and returns the newer car.
4. Write a function that creates a deep copy of an object (without using JSON
methods).
1. Create an array of person objects, each with properties for name, age, and
occupation.
2. Write functions to:
3. Find the average age of all people
4. Group people by occupation
5. Find the oldest person
7. Create a function that takes an array of objects and a property name, and returns a
new array sorted by that property.
Exercise 4: JSON
1. Create an array of product objects, each with properties for id, name, price, and
quantity.
2. Implement functions to:
3. Add a new product
4. Remove a product by id
5. Update a product's quantity
6. Calculate the total value of the inventory
7. Find products with low stock (quantity < 5)
8. Generate a report of the inventory as a JSON string
By completing these exercises, you'll gain practical experience with JavaScript arrays
and objects and be well-prepared for the more advanced topics in the upcoming
chapters.
When a browser loads an HTML document, it creates a model of that document called
the DOM. This model is organized as a tree of objects:
Document
|
HTML
/ \
HEAD BODY
/ \ / \
TITLE META H1 DIV
| | / \
"Title" "Heading" P SPAN
| |
"Text" "More text"
Types of Nodes
DOM Interfaces
The DOM provides various interfaces (object types) that you can use to work with the
document:
Let's look at a simple HTML document and its corresponding DOM structure:
<!DOCTYPE html>
<html>
<head>
<title>DOM Example</title>
</head>
<body>
<h1 id="main-heading">Welcome to the DOM</h1>
<p>This is a <span class="highlight">paragraph</span> about
the DOM.</p>
<div class="container">
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
</body>
</html>
In this example: - The document object is at the root - The <html> element is the root
element - The <head> and <body> are child elements of <html> - The <h1> , <p> ,
and <div> are child elements of <body> - The text "Welcome to the DOM" is a text
node child of the <h1> element - And so on...
Before diving deeper into the DOM, it's important to understand the window object,
which represents the browser window. The window object is the global object in
browser-based JavaScript and provides access to:
6.2.1 getElementById
This method returns a single element (or null if no matching element is found)
because IDs should be unique within a document.
6.2.2 getElementsByClassName
6.2.3 getElementsByTagName
These modern methods use CSS selectors to select elements, making them very
powerful and flexible.
querySelector
The querySelector method returns the first element that matches the specified CSS
selector:
querySelectorAll
The querySelectorAll method returns all elements that match the specified CSS
selector:
1. HTMLCollection:
2. Live collection (automatically updates when the DOM changes)
3. No built-in forEach method
5. NodeList:
You can also select elements within other elements by calling these methods on an
element instead of the document:
This is useful for limiting the scope of your selection and improving performance when
working with large documents.
textContent
The textContent property gets or sets the text content of an element and all its
descendants:
textContent treats all content as plain text, even if you include HTML tags.
innerHTML
The innerHTML property gets or sets the HTML content within an element:
const paragraph = document.querySelector('p');
innerHTML parses the content as HTML, which means it can create new elements from
the provided string. However, this can pose security risks if you're inserting user-
provided content (potential XSS attacks).
innerText
Key differences from textContent : - innerText is aware of CSS styling and won't
return text that is hidden with CSS - innerText triggers a reflow (layout calculation)
which can impact performance - textContent returns all text content, regardless of
CSS visibility
You can get, set, check, and remove attributes using various methods and properties:
Standard Attributes
// Get attribute
console.log(link.href); // "https://example.com"
console.log(link.getAttribute('href')); // "https://example.com"
// Set attribute
link.href = "https://javascript.info";
link.setAttribute('href', 'https://javascript.info');
// Remove attribute
link.removeAttribute('target');
Data Attributes
HTML5 introduced data attributes, which allow you to store custom data on elements:
Class Manipulation
// Add classes
element.classList.add('highlight', 'active');
// Remove classes
element.classList.remove('active');
You can also set the entire className property, but this replaces all existing classes:
You can modify an element's style directly using the style property:
console.log(computedStyle.width); // "500px"
console.log(computedStyle.display); // "block"
console.log(computedStyle.getPropertyValue('margin-top')); //
"20px"
The DOM API allows you to create new elements and add them to the document.
Creating Elements
// Add attributes
newParagraph.id = 'dynamic-paragraph';
newParagraph.classList.add('highlight');
// Create a comment
const comment = document.createComment('This is a comment');
Key differences between append and appendChild : - append can add multiple
nodes and text strings - appendChild can only add one node and returns the
appended node - append doesn't return anything
Cloning Elements
Removing Elements
// Modern method
elementToRemove.remove();
parent.replaceChild(newElement, oldElement);
function createDynamicList(items) {
// Create container
const container = document.createElement('div');
container.className = 'dynamic-list-container';
// Create heading
const heading = document.createElement('h2');
heading.textContent = 'Dynamic List';
container.appendChild(heading);
// Create list
const list = document.createElement('ul');
list.className = 'dynamic-list';
listItem.appendChild(deleteButton);
list.appendChild(listItem);
});
container.appendChild(list);
// Add to document
document.body.appendChild(container);
}
// Usage
createDynamicList(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
Parent Relationships
Child Relationships
Sibling Relationships
Let's create a simple function that visualizes the DOM structure of an element:
// Usage
const container = document.querySelector('.container');
visualizeDOMTree(container);
Summary
• Understanding the DOM: The DOM tree structure, node types, and interfaces
• Selecting DOM Elements: Methods like getElementById ,
getElementsByClassName , getElementsByTagName , querySelector , and
querySelectorAll
• Manipulating DOM Elements: Changing content, attributes, and styles
• Creating and Removing Elements: Adding new elements to the DOM and
removing existing ones
• Traversing the DOM: Navigating through parent, child, and sibling relationships
The DOM is a fundamental concept in web development, serving as the bridge between
your HTML and JavaScript. By manipulating the DOM, you can create dynamic,
interactive web pages that respond to user actions and update in real-time.
Exercises
Create an HTML page with various elements and practice selecting them:
By completing these exercises, you'll gain practical experience with DOM manipulation
and be well-prepared for the next chapter on events, which will allow you to make your
web pages truly interactive.
Chapter 7: Events
Events are actions or occurrences that happen in the browser, such as a user clicking a
button, pressing a key, or a page finishing loading. JavaScript can detect and respond to
these events, allowing you to create interactive web applications. In this chapter, we'll
explore how to work with events in JavaScript.
Mouse Events
Keyboard Events
Form Events
Document/Window Events
• load : Occurs when a resource and its dependent resources have finished loading
• DOMContentLoaded : Occurs when the initial HTML document has been
completely loaded and parsed
• resize : Occurs when the document view is resized
• scroll : Occurs when the document view is scrolled
• unload : Occurs when a page is being unloaded (or the browser window is closed)
• beforeunload : Occurs before the document is about to be unloaded
Touch Events
Drag Events
Media Events
Transition Events
<script>
function handleClick() {
alert('Button clicked!');
}
</script>
While this approach is simple, it's generally not recommended because: - It mixes HTML
and JavaScript, which is poor separation of concerns - It can only attach one handler per
event type - It has limited access to event information
button.onclick = function() {
alert('Button clicked!');
};
This approach is better than HTML attributes, but still has limitations: - You can only
attach one handler per event type - If you assign a new handler, it overwrites the
previous one
Event Listeners
The most flexible way to handle events is using the addEventListener method:
button.addEventListener('click', function() {
alert('Button clicked!');
});
Advantages of event listeners: - You can add multiple listeners for the same event - More
control over event propagation - Can be removed when no longer needed
button.addEventListener('click', function() {
console.log('Second handler');
});
Note that you must provide the same function reference to remove the listener.
Anonymous functions cannot be removed directly:
button.addEventListener('click', handleClick);
// Later
button.removeEventListener('click', handleClick);
button.addEventListener('click', handleClick, {
once: true, // The listener will be automatically
removed after it triggers once
capture: true, // The event will be captured in the
capturing phase
passive: true // The listener will never call
preventDefault()
});
The once option is particularly useful for events that should only happen once:
// Using addEventListener
button.addEventListener('click', function(event) {
console.log(event.type); // "click"
});
The event object has many properties, some of which are common to all events:
button.addEventListener('click', function(event) {
console.log(event.type); // The event type (e.g.,
"click")
console.log(event.target); // The element that triggered
the event
console.log(event.currentTarget); // The element that the
event listener is attached to
console.log(event.timeStamp); // The time when the event
occurred
console.log(event.bubbles); // Whether the event bubbles
up through the DOM
console.log(event.cancelable); // Whether the event can be
canceled
console.log(event.defaultPrevented); // Whether
preventDefault() was called
});
Event-Specific Properties
element.addEventListener('click', function(event) {
console.log(event.clientX, event.clientY); // Coordinates
relative to the viewport
console.log(event.pageX, event.pageY); // Coordinates
relative to the document
console.log(event.screenX, event.screenY); // Coordinates
relative to the screen
console.log(event.button); // Which mouse button was
pressed (0: left, 1: middle, 2: right)
console.log(event.buttons); // Bitmask of buttons
currently pressed
console.log(event.altKey); // Whether the Alt key was
pressed
console.log(event.ctrlKey); // Whether the Ctrl key was
pressed
console.log(event.shiftKey); // Whether the Shift key was
pressed
console.log(event.metaKey); // Whether the Meta key was
pressed (Command on Mac)
});
document.addEventListener('keydown', function(event) {
console.log(event.key); // The key value (e.g., "a",
"Enter")
console.log(event.code); // The physical key code
(e.g., "KeyA", "Enter")
console.log(event.keyCode); // The key code (deprecated)
console.log(event.altKey); // Whether the Alt key was
pressed
console.log(event.ctrlKey); // Whether the Ctrl key was
pressed
console.log(event.shiftKey); // Whether the Shift key was
pressed
console.log(event.metaKey); // Whether the Meta key was
pressed
console.log(event.repeat); // Whether the key is being
held down
});
form.addEventListener('submit', function(event) {
// For submit events, the form element is available
console.log(event.target); // The form element
});
input.addEventListener('change', function(event) {
console.log(event.target.value); // The new value of the
input
});
Event Methods
The event object also provides methods to control the event's behavior:
button.addEventListener('click', function(event) {
// Prevent the default action
event.preventDefault();
preventDefault()
The preventDefault() method stops the browser from executing the default action
of an event. Common uses include:
stopPropagation()
childElement.addEventListener('click', function(event) {
event.stopPropagation();
console.log('Child clicked');
// This event won't reach the parent
});
parentElement.addEventListener('click', function() {
console.log('Parent clicked');
// This won't run when child is clicked
});
stopImmediatePropagation()
button.addEventListener('click', function(event) {
console.log('First handler');
event.stopImmediatePropagation();
// No other handlers will be called
});
button.addEventListener('click', function() {
console.log('Second handler');
// This won't run
});
7.4 Event Propagation
When an event occurs on an element that has parent elements, modern browsers run
three different phases:
1. Capturing Phase: The event travels from the window down to the target element
2. Target Phase: The event reaches the target element
3. Bubbling Phase: The event bubbles up from the target element back to the
window
Event Bubbling
By default, most events bubble up from the target element through its ancestors:
<div id="outer">
<div id="inner">
<button id="button">Click Me</button>
</div>
</div>
<script>
document.getElementById('button').addEventListener('click',
function() {
console.log('Button clicked');
});
document.getElementById('inner').addEventListener('click',
function() {
console.log('Inner div clicked');
});
document.getElementById('outer').addEventListener('click',
function() {
console.log('Outer div clicked');
});
</script>
Button clicked
Inner div clicked
Outer div clicked
This happens because the click event first triggers on the button, then bubbles up to its
parent elements.
Event Capturing
You can also capture events as they travel down to the target by setting the third
parameter of addEventListener to true or { capture: true } :
document.getElementById('outer').addEventListener('click',
function() {
console.log('Outer div - Capture phase');
}, true);
document.getElementById('inner').addEventListener('click',
function() {
console.log('Inner div - Capture phase');
}, true);
document.getElementById('button').addEventListener('click',
function() {
console.log('Button - Capture phase');
}, true);
document.getElementById('button').addEventListener('click',
function() {
console.log('Button - Bubble phase');
});
document.getElementById('inner').addEventListener('click',
function() {
console.log('Inner div - Bubble phase');
});
document.getElementById('outer').addEventListener('click',
function() {
console.log('Outer div - Bubble phase');
});
The complete event flow is: 1. Capture phase: Window → Document → HTML → Body
→ ... → Target's Parent 2. Target phase: Target element itself 3. Bubble phase: Target's
Parent → ... → Body → HTML → Document → Window
Non-Bubbling Events
Not all events bubble. Some examples of non-bubbling events include: - focus and
blur (use focusin and focusout for bubbling versions) - load and unload -
mouseenter and mouseleave (use mouseover and mouseout for bubbling
versions) - resize and scroll (usually)
Instead of attaching click handlers to each button in a list, attach one to the parent:
<ul id="menu">
<li><button data-action="save">Save</button></li>
<li><button data-action="load">Load</button></li>
<li><button data-action="delete">Delete</button></li>
</ul>
<script>
document.getElementById('menu').addEventListener('click',
function(event) {
// Check if a button was clicked
if (event.target.tagName === 'BUTTON') {
const action = event.target.dataset.action;
switch (action) {
case 'save':
console.log('Saving...');
break;
case 'load':
console.log('Loading...');
break;
case 'delete':
console.log('Deleting...');
break;
}
}
});
</script>
// The existing event handler will work for this new button
The closest() method is useful for finding the nearest ancestor that matches a
selector:
<ul id="todo-list">
<li>
<span class="todo-text">Buy groceries</span>
<button class="delete-btn">Delete</button>
<button class="edit-btn">Edit</button>
</li>
<!-- More list items -->
</ul>
<script>
document.getElementById('todo-list').addEventListener('click',
function(event) {
// Find which button was clicked
if (event.target.classList.contains('delete-btn')) {
// Find the parent li element
const listItem = event.target.closest('li');
console.log('Deleting:', listItem.querySelector('.todo-
text').textContent);
listItem.remove();
} else if (event.target.classList.contains('edit-btn')) {
const listItem = event.target.closest('li');
console.log('Editing:', listItem.querySelector('.todo-
text').textContent);
// Edit functionality
}
});
</script>
Event delegation works with most event types, not just clicks:
document.getElementById('form').addEventListener('input',
function(event) {
if (event.target.tagName === 'INPUT') {
console.log(`Input ${event.target.name} changed to: $
{event.target.value}`);
}
});
document.getElementById('form').addEventListener('submit',
function(event) {
event.preventDefault();
console.log('Form submitted');
});
The detail property can contain any data you want to pass with the event.
You listen for custom events the same way as built-in events:
document.addEventListener('userLoggedIn', function(event) {
console.log(`User ${event.detail.username} logged in at $
{event.detail.timestamp}`);
});
You can also use the older Event constructor for simpler events:
document.dispatchEvent(event);
}
getItemCount() {
return 5; // Simplified for example
}
getTotalPrice() {
return 99.95; // Simplified for example
}
}
updateSummary(event) {
console.log(`Cart updated: ${event.detail.itemCount}
items, $${event.detail.totalPrice}`);
console.log(`Last item added: $
{event.detail.lastItemAdded.name}`);
// Update UI
document.getElementById('cart-count').textContent =
event.detail.itemCount;
document.getElementById('cart-total').textContent = `$$
{event.detail.totalPrice}`;
}
}
// Initialize components
const cart = new ShoppingCart();
const summary = new CartSummary();
// Add an item
cart.addItem({ id: 1, name: 'JavaScript Book', price: 29.99 });
When handling events that fire rapidly (like scroll , resize , or mousemove ), it's
often necessary to limit how frequently your event handler executes.
Debouncing
Debouncing ensures that a function is only executed after a certain amount of time has
passed since it was last invoked:
return function(...args) {
// Clear the previous timeout
clearTimeout(timeoutId);
// Usage
const handleSearch = debounce(function(event) {
console.log('Searching for:', event.target.value);
// Perform search operation
}, 300);
document.getElementById('search-
input').addEventListener('input', handleSearch);
This is useful for search inputs, where you want to wait until the user stops typing before
performing the search.
Throttling
Throttling ensures that a function is executed at most once in a specified time period:
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage
const handleScroll = throttle(function() {
console.log('Scroll position:', window.scrollY);
// Update something based on scroll position
}, 100);
window.addEventListener('scroll', handleScroll);
This is useful for scroll or resize events, where you want to limit the number of times
your handler executes.
In single page applications (SPAs), you often need to add and remove event listeners as
components mount and unmount:
class Component {
constructor(element) {
this.element = element;
this.boundHandleClick = this.handleClick.bind(this);
this.init();
}
init() {
// Add event listeners
this.element.addEventListener('click',
this.boundHandleClick);
}
handleClick(event) {
console.log('Component clicked');
}
destroy() {
// Clean up event listeners when component is destroyed
this.element.removeEventListener('click',
this.boundHandleClick);
}
}
// Usage
const component = new Component(document.getElementById('my-
component'));
You can wrap event handling in promises for more readable asynchronous code:
// Usage
async function handleFormSubmission() {
console.log('Waiting for form submission...');
console.log('Form submitted');
// Process form data
}
handleFormSubmission();
Keyboard Accessibility
When handling click events, consider keyboard users who navigate with the Tab key and
activate elements with the Enter or Space keys:
button.addEventListener('click', handleAction);
function handleAction(event) {
console.log('Action triggered');
// Perform action
}
Summary
• Event Types: The various types of events in JavaScript, from mouse and keyboard
events to form, document, and custom events
• Event Handlers: Different ways to assign event handlers, including HTML
attributes, DOM properties, and event listeners
• The Event Object: How to access and use the event object to get information
about the event and control its behavior
• Event Propagation: Understanding the capturing and bubbling phases of event
propagation
• Event Delegation: Using event bubbling to handle events efficiently with a single
listener on a parent element
• Custom Events: Creating and dispatching your own events for component
communication
• Practical Patterns: Techniques like debouncing, throttling, and promise-based
event handling
Events are a fundamental part of creating interactive web applications. They allow your
JavaScript code to respond to user actions and other occurrences in the browser. By
mastering events, you can create responsive, user-friendly interfaces that provide a great
user experience.
Exercises
1. Create a nested set of elements and demonstrate event bubbling by logging which
elements receive a click event
2. Modify the example to use the capturing phase instead
3. Create a situation where stopPropagation() is useful
4. Create a situation where preventDefault() is useful
5. Create a situation where stopImmediatePropagation() is useful
1. Create a to-do list where new items can be added dynamically, and all items can be
checked off or deleted using event delegation
2. Create a table where clicking a column header sorts the table by that column using
event delegation
3. Create a form with multiple input fields that validates each field on blur using event
delegation
4. Create a tabbed interface where clicking a tab shows the corresponding content
using event delegation
5. Create a dynamic grid of items where each item can be clicked to show details
using event delegation
1. Create a custom event that fires when a user has been inactive for a certain period
2. Create a custom event for a shopping cart that fires when items are added or
removed
3. Create a custom event system for a simple pub/sub (publish/subscribe) pattern
4. Create a custom drag-and-drop system using custom events
5. Create a form validation system that uses custom events to communicate
validation errors
By completing these exercises, you'll gain practical experience with JavaScript events
and be well-prepared for the next chapter on advanced objects and prototypes.
1. Encapsulation
Encapsulation is the bundling of data and methods that operate on that data within a
single unit (object):
// Basic encapsulation
const person = {
// Data (properties)
firstName: "John",
lastName: "Doe",
age: 30,
// Methods
getFullName() {
return `${this.firstName} ${this.lastName}`;
},
greet() {
return `Hello, my name is ${this.getFullName()}`;
}
};
2. Abstraction
Abstraction means hiding complex implementation details and showing only the
necessary features:
// Public API
connect() {
console.log("Establishing database connection...");
this._connection = { status: "connected", id:
Date.now() };
return true;
},
insert(item) {
if (!this._connection) {
throw new Error("Not connected to database");
}
this._data.push(item);
return true;
},
find(query) {
return this._data.filter(item => {
for (let key in query) {
if (item[key] !== query[key]) {
return false;
}
}
return true;
});
}
};
// Usage
database.connect();
database.insert({ id: 1, name: "John" });
database.insert({ id: 2, name: "Jane" });
const results = database.find({ name: "John" });
console.log(results); // [{ id: 1, name: "John" }]
3. Inheritance
Inheritance allows objects to inherit properties and methods from other objects:
dog.makeSound(); // "Woof!"
cat.makeSound(); // "Meow!"
4. Polymorphism
Polymorphism allows methods to do different things based on the object they are acting
upon:
function makeAnimalSound(animal) {
animal.makeSound();
}
makeAnimalSound(dog); // "Woof!"
makeAnimalSound(cat); // "Meow!"
const person = {
name: "John",
age: 30,
greet() {
return `Hello, my name is ${this.name}`;
}
};
2. Constructor Functions
Constructor functions are used to create multiple objects with the same structure:
3. Object.create()
The Object.create() method creates a new object with the specified prototype
object:
const personProto = {
greet() {
return `Hello, my name is ${this.name}`;
}
};
ES6 introduced class syntax, which is syntactic sugar over JavaScript's prototype-based
inheritance:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name}`;
}
}
this.start = function() {
this.isRunning = true;
return `${this.make} ${this.model} started`;
};
this.stop = function() {
this.isRunning = false;
return `${this.make} ${this.model} stopped`;
};
}
When you use the new keyword with a constructor function, several things happen:
function Person(name) {
this.name = name;
}
// Using new
const john = new Person("John");
console.log(john.name); // "John"
function Person(name) {
"use strict";
this.name = name;
}
function Person(name) {
if (!(this instanceof Person)) {
return new Person(name);
}
this.name = name;
}
To save memory, move methods to the prototype instead of creating them in each
instance:
Car.prototype.stop = function() {
this.isRunning = false;
return `${this.make} ${this.model} stopped`;
};
When you try to access a property or method of an object, JavaScript first looks for it
directly on the object. If it doesn't find it, it looks at the object's prototype, then that
object's prototype, and so on, forming what's called the "prototype chain".
// Using Object.getPrototypeOf()
const dogProto = Object.getPrototypeOf(dog);
console.log(dogProto === animal); // true
// Using Object.create()
const animal = { eats: true };
const dog = Object.create(animal);
console.log(dog.eats); // true
Prototype-Based Inheritance
// Parent constructor
function Animal(name) {
this.name = name;
}
// Parent methods
Animal.prototype.eat = function() {
return `${this.name} is eating`;
};
// Child constructor
function Dog(name, breed) {
// Call parent constructor
Animal.call(this, name);
this.breed = breed;
}
// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Fix the constructor property
// Add child methods
Dog.prototype.bark = function() {
return `${this.name} says woof!`;
};
// Create instances
const generic = new Animal("Generic Animal");
const rex = new Dog("Rex", "German Shepherd");
The instanceof operator tests whether an object has a constructor's prototype in its
prototype chain:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
greet() {
return `Hello, my name is ${this.getFullName()}`;
}
}
greet() {
return `Hello, my name is ${this.name}`;
}
};
Classes can inherit from other classes using the extends keyword:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a noise`;
}
}
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a noise`;
}
}
Class Hoisting
// This works
const p1 = new FunctionPerson("John");
function FunctionPerson(name) {
this.name = name;
}
class Person {
constructor(firstName, lastName) {
this._firstName = firstName;
this._lastName = lastName;
}
// Getter
get fullName() {
return `${this._firstName} ${this._lastName}`;
}
// Setter
set fullName(value) {
const parts = value.split(' ');
this._firstName = parts[0];
this._lastName = parts[1] || '';
}
// Regular method
greet() {
return `Hello, my name is ${this.fullName}`;
}
}
class Greeter {
[methodName]() {
return "Hello!";
}
[`say${"Hi"}`]() {
return "Hi!";
}
}
Static Methods
class MathUtils {
static add(x, y) {
return x + y;
}
static subtract(x, y) {
return x - y;
}
static multiply(x, y) {
return x * y;
}
}
Static Properties
class Config {
static API_URL = "https://api.example.com";
static VERSION = "1.0.0";
static AUTHOR = "John Doe";
}
console.log(Config.API_URL); // "https://api.example.com"
console.log(Config.VERSION); // "1.0.0"
console.log(Config.AUTHOR); // "John Doe"
1. Factory Methods
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// Factory method
static createFromFullName(fullName) {
const [firstName, lastName] = fullName.split(' ');
return new Person(firstName, lastName);
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
}
2. Utility Methods
class StringUtils {
static capitalize(str) {
return str.charAt(0).toUpperCase() +
str.slice(1).toLowerCase();
}
static reverse(str) {
return str.split('').reverse().join('');
}
static countVowels(str) {
return (str.match(/[aeiou]/gi) || []).length;
}
}
console.log(StringUtils.capitalize("hello")); // "Hello"
console.log(StringUtils.reverse("hello")); // "olleh"
console.log(StringUtils.countVowels("hello")); // 2
3. Singleton Pattern
class Database {
static instance = null;
constructor() {
if (Database.instance) {
return Database.instance;
}
this.data = [];
Database.instance = this;
}
add(item) {
this.data.push(item);
}
getAll() {
return this.data;
}
static getInstance() {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
}
db1.add("Item 1");
db2.add("Item 2");
Private Fields
Private fields are declared with a hash ( # ) prefix and are only accessible within the
class:
class BankAccount {
// Private fields
#balance = 0;
#accountNumber;
constructor(accountNumber, initialBalance) {
this.#accountNumber = accountNumber;
if (initialBalance > 0) {
this.#balance = initialBalance;
}
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return true;
}
return false;
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
return true;
}
return false;
}
get balance() {
return this.#balance;
}
get accountInfo() {
return `Account ${this.#accountNumber}: $$
{this.#balance}`;
}
}
account.deposit(500);
console.log(account.balance); // 1500
account.withdraw(200);
console.log(account.balance); // 1300
Private Methods
class PaymentProcessor {
#apiKey;
constructor(apiKey) {
this.#apiKey = apiKey;
}
// Public method
processPayment(amount, currency) {
console.log(`Processing payment of ${amount} ${currency}
`);
// Validate inputs
if (!this.#validateAmount(amount)) {
throw new Error("Invalid amount");
}
if (!this.#validateCurrency(currency)) {
throw new Error("Invalid currency");
}
// Process payment
const response = this.#sendPaymentRequest(amount,
currency);
return response;
}
// Private methods
#validateAmount(amount) {
return typeof amount === 'number' && amount > 0;
}
#validateCurrency(currency) {
const validCurrencies = ['USD', 'EUR', 'GBP'];
return validCurrencies.includes(currency);
}
#sendPaymentRequest(amount, currency) {
// In a real implementation, this would make an API call
console.log(`Sending payment request with API key: $
{this.#apiKey}`);
return { success: true, id: `payment_${Date.now()}` };
}
}
class AuthService {
// Private static field
static #instance = null;
constructor() {
if (AuthService.#instance) {
return AuthService.#instance;
}
AuthService.#instance = this;
}
setToken(token) {
if (AuthService.#validateToken(token)) {
this.#token = token;
return true;
}
return false;
}
getToken() {
return this.#token;
}
}
Summary
Exercises
1. Create a constructor function Vehicle with properties for make , model , and
year .
2. Add methods to the Vehicle prototype for start() , stop() , and
getInfo() .
3. Create two different vehicle instances and demonstrate their methods.
4. Create a Motorcycle constructor that inherits from Vehicle and adds a
wheelCount property.
5. Override the getInfo() method in Motorcycle to include the wheel count.
1. Create a set of small, focused objects with specific behaviors (e.g., logger ,
eventEmitter , storage ).
2. Implement a function compose(target, ...sources) that copies methods
from source objects to a target object.
3. Use your composition function to create a complex object with behaviors from
multiple sources.
4. Demonstrate how this approach differs from inheritance and discuss its
advantages.
1. Media (base class with common properties like title , year , and
isAvailable )
2. Book (extends Media with properties like author and pages )
3. DVD (extends Media with properties like director and duration )
4. Library (manages a collection of media items with methods to add, remove,
search, checkout, and return items)
Implement the system using ES6 classes with appropriate inheritance, private fields,
static methods, and getters/setters. Include validation to ensure data integrity and
methods to display information about the library's collection.
By completing these exercises, you'll gain practical experience with JavaScript's object-
oriented features and be well-prepared for the next chapter on closures and scopes.
Variables declared outside any function or block have global scope. They can be
accessed from anywhere in your JavaScript code, including inside functions and blocks.
function someFunction() {
console.log(globalVariable); // "I'm global"
}
someFunction();
console.log(globalVariable); // "I'm global"
In browsers, global variables become properties of the window object. In Node.js, they
become properties of the global object.
// In a browser
var x = 10;
console.log(window.x); // 10
While global variables are easy to use, they can lead to several problems:
1. Name collisions: Different parts of your code or external libraries might use the
same variable name
2. Unintended modifications: Any part of your code can change global variables
3. Difficulty in testing: Code with many global variables is harder to test
4. Reduced readability: It's harder to understand where variables are defined and
used
Variables declared inside a function are only accessible within that function. They have
function scope.
function showMessage() {
const message = "Hello, world!"; // Function-scoped variable
console.log(message);
}
When functions are nested, inner functions have access to variables declared in their
outer functions:
function outer() {
const outerVar = "I'm from outer function";
function inner() {
const innerVar = "I'm from inner function";
console.log(outerVar); // "I'm from outer function"
}
inner();
// console.log(innerVar); // ReferenceError: innerVar is not
defined
}
outer();
Variable Shadowing
When a variable in an inner scope has the same name as a variable in an outer scope, the
inner variable "shadows" the outer one:
function checkScope() {
const value = "local";
console.log(value); // "local"
}
checkScope();
console.log(value); // "global"
Introduced in ES6, let and const declarations are block-scoped, meaning they are
only accessible within the block (enclosed by curly braces) in which they are defined.
if (true) {
let blockScoped = "I'm block-scoped";
const alsoBlockScoped = "I'm also block-scoped";
var notBlockScoped = "I'm function-scoped";
// Using var
function createFunctionsVar() {
var functions = [];
return functions;
}
// Using let
function createFunctionsLet() {
const functions = [];
return functions;
}
With var , all functions reference the same variable i , which has a final value of 3. With
let , each iteration creates a new binding for i , so each function captures a different
value.
function outer() {
const outerVar = "outer";
function inner() {
const innerVar = "inner";
console.log(globalVar); // "global"
console.log(outerVar); // "outer"
console.log(innerVar); // "inner"
}
inner();
}
outer();
In this example, inner() has access to variables in its own scope, the outer()
function's scope, and the global scope. This is because of lexical scoping - the scope is
determined by where the function is defined, not where it's called.
Scope Chain
When you reference a variable, JavaScript looks for it in the current scope. If it doesn't
find it, it looks in the outer scope, and so on, until it reaches the global scope. This
sequence of scopes is called the scope chain.
function outer() {
const outer = "outer";
function inner() {
const inner = "inner";
function innermost() {
// Scope chain: innermost -> inner -> outer ->
global
console.log(inner); // "inner"
console.log(outer); // "outer"
console.log(global); // "global"
}
innermost();
}
inner();
}
outer();
If JavaScript can't find a variable anywhere in the scope chain, it throws a
ReferenceError .
9.3 Closures
A closure is a function that remembers its lexical scope even when the function is
executed outside that scope. This is one of JavaScript's most powerful features.
A closure is created when an inner function is made accessible outside of the function in
which it was defined:
function createCounter() {
let count = 0; // This variable is "closed over"
return function() {
count++; // The inner function has access to count
return count;
};
}
In this example, the inner function returned by createCounter() forms a closure over
the count variable. Even after createCounter() has finished executing, the inner
function still has access to count .
Each call to a function that returns a closure creates a new closure with its own separate
environment:
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (separate count variable)
console.log(counter1()); // 3
console.log(counter2()); // 2
function createGreeter(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}
Closures in Loops
Closures are incredibly useful in JavaScript. Here are some common applications:
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private variable
return {
deposit: function(amount) {
if (amount > 0) {
balance += amount;
return true;
}
return false;
},
withdraw: function(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
},
getBalance: function() {
return balance;
}
};
}
Function Factories
function multiply(a) {
return function(b) {
return a * b;
};
}
console.log(double(5)); // 10
console.log(triple(5)); // 15
Memoization
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
console.log("Returning from cache");
return cache[key];
}
console.log("Computing result");
const result = fn(...args);
cache[key] = result;
return result;
};
}
// Memoized version
const memoizedFibonacci = memoize(function(n) {
if (n <= 1) return n;
return memoizedFibonacci(n - 1) + memoizedFibonacci(n - 2);
});
console.time("First call");
console.log(memoizedFibonacci(40)); // Takes some time
console.timeEnd("First call");
console.time("Second call");
console.log(memoizedFibonacci(40)); // Much faster (from cache)
console.timeEnd("Second call");
Event Handlers
Closures are commonly used in event handlers to access variables from the containing
scope:
button.addEventListener("click", function() {
// The event handler forms a closure over the message
variable
alert(message);
});
}
Module Pattern
Before ES6 modules, closures were used to create modules with private and public parts:
const calculator = (function() {
// Private variables and functions
let result = 0;
function validateNumber(num) {
return typeof num === "number" && !isNaN(num);
}
// Public API
return {
add: function(num) {
if (validateNumber(num)) {
result += num;
}
return this; // For method chaining
},
subtract: function(num) {
if (validateNumber(num)) {
result -= num;
}
return this;
},
multiply: function(num) {
if (validateNumber(num)) {
result *= num;
}
return this;
},
divide: function(num) {
if (validateNumber(num) && num !== 0) {
result /= num;
}
return this;
},
getResult: function() {
return result;
},
reset: function() {
result = 0;
return this;
}
};
})();
console.log(calculator.add(5).multiply(2).subtract(3).getResult()); //
7
calculator.reset();
console.log(calculator.getResult()); // 0
Currying
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
} else {
return function(...moreArgs) {
return curried(...args, ...moreArgs);
};
}
};
}
function add(a, b, c) {
return a + b + c;
}
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6
function increment() {
count++;
}
function decrement() {
count--;
}
// Public API
return {
increment: function() {
increment();
},
decrement: function() {
decrement();
},
getCount: function() {
return count;
},
reset: function() {
count = 0;
}
};
})();
counterModule.increment();
counterModule.increment();
console.log(counterModule.getCount()); // 2
counterModule.decrement();
console.log(counterModule.getCount()); // 1
counterModule.reset();
console.log(counterModule.getCount()); // 0
A variation of the module pattern is the revealing module pattern, which defines all
functions and variables privately and then exposes only what should be public:
// Private function
function privateFunction() {
console.log("This is private");
}
// Public function
function publicFunction() {
console.log("This is public");
privateFunction();
}
Module Augmentation
return {
add: function(num) {
result += num;
return this;
},
subtract: function(num) {
result -= num;
return this;
},
getResult: function() {
return result;
}
};
})();
module.divide = function(num) {
if (num !== 0) {
const currentResult = module.getResult();
module.add(currentResult / num - currentResult);
}
return this;
};
})(calculatorModule);
console.log(calculatorModule.add(5).multiply(2).subtract(3).getResult());
7
With the introduction of ES6 modules, the module pattern is less necessary, but
understanding it is still important:
// math.js
// Private (not exported)
const PI = 3.14159;
function square(x) {
return x * x;
}
// Public (exported)
export function calculateCircleArea(radius) {
return PI * square(radius);
}
// main.js
import { calculateCircleArea, calculateCircleCircumference }
from './math.js';
console.log(calculateCircleArea(5)); // ~78.54
console.log(calculateCircleCircumference(5)); // ~31.42
JavaScript uses automatic garbage collection to free memory that's no longer needed.
The basic principle is that if an object is not reachable from the root (global object), it
can be garbage collected.
function createObject() {
const obj = { name: "Temporary" };
return obj;
}
function setupHandler() {
const element = document.getElementById("myElement");
const largeData = new Array(1000000).fill("data");
element.addEventListener("click", function() {
// This closure holds a reference to largeData
console.log("Element clicked", largeData.length);
});
}
setupHandler();
// Even if the element is removed from the DOM, the event
handler
// and its closure (including largeData) remain in memory
function setupHandler() {
const element = document.getElementById("myElement");
const largeData = new Array(1000000).fill("data");
function clickHandler() {
console.log("Element clicked");
// Note: largeData is not referenced here
}
element.addEventListener("click", clickHandler);
Summary
• Scope: The rules that determine where variables are accessible in your code
• Global scope: Variables accessible throughout your program
• Function scope: Variables accessible only within a function
• Block scope: Variables (declared with let and const ) accessible only within a
block
• Closures: Functions that remember their lexical scope even when executed outside
that scope
• The Module Pattern: Using closures to create private and public parts of a module
Understanding scope and closures is essential for writing effective JavaScript code.
These concepts underpin many JavaScript patterns and techniques, from data privacy to
functional programming approaches.
Exercises
Exercise 1: Scope
1. Create a function that demonstrates the difference between var , let , and
const in terms of scope.
2. Write a function that contains nested functions, each accessing variables from
different scopes.
3. Create an example that demonstrates variable shadowing.
4. Write a function that shows how block scope works in loops and conditional
statements.
5. Create a function that demonstrates how the global scope interacts with function
scope.
Exercise 2: Closures
1. Create a counter function using closures that increments and returns a count
variable.
2. Write a function that returns multiple functions, each of which has access to the
same closure.
3. Create a function that demonstrates how closures can be used for data privacy.
4. Write a function that uses closures to create a sequence generator.
5. Create a memoization function that caches the results of expensive function calls.
1. Create a simple module that manages a to-do list with add, remove, and list
functions.
2. Implement the revealing module pattern for a shopping cart module.
3. Create a module for a quiz application with private questions and public methods
to take the quiz.
4. Write a module that manages a user's authentication state.
5. Create a module that can be augmented with additional functionality after its
initial definition.
Create a small library for managing asynchronous tasks that uses closures to track the
state of each task. The library should include:
By completing these exercises, you'll gain practical experience with JavaScript's scope
and closure mechanisms and be well-prepared for the next chapter on AJAX requests
and the Fetch API.
Benefits of AJAX
Limitations of AJAX
• Browser History: AJAX requests don't automatically update the browser history
• Bookmarking: Dynamic content loaded via AJAX isn't reflected in the URL by
default
• Search Engine Optimization: Content loaded via AJAX might not be indexed by
search engines
• Same-Origin Policy: By default, AJAX requests are restricted to the same domain
10.2 XMLHttpRequest
The XMLHttpRequest (XHR) object is the traditional way to make AJAX requests. While
newer alternatives like the Fetch API exist, understanding XHR is still important for
working with legacy code and understanding AJAX fundamentals.
Important Properties
Important Methods
xhr.onload = function() {
if (xhr.status === 200) {
// No need to parse JSON, it's already parsed
const data = xhr.response;
console.log('Data:', data);
}
};
xhr.send();
XHR allows you to monitor the progress of requests, which is useful for large file uploads
or downloads:
// Progress event
xhr.onprogress = function(event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) *
100;
console.log(`Progress: ${percentComplete.toFixed(2)}%`);
// Clean up
URL.revokeObjectURL(url);
}
};
xhr.send();
Handling Timeouts
// Timeout handler
xhr.ontimeout = function() {
console.error('Request timed out');
};
xhr.onload = function() {
if (xhr.status === 200) {
console.log('Response:', xhr.responseText);
}
};
xhr.send();
Instead of using properties like onload and onerror , you can use event listeners:
xhr.addEventListener('error', function() {
console.error('Request failed');
});
xhr.addEventListener('progress', function(event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) *
100;
console.log(`Progress: ${percentComplete.toFixed(2)}%`);
}
});
xhr.send();
10.3 Fetch API
The Fetch API is a modern replacement for XMLHttpRequest. It provides a more powerful
and flexible feature set and is based on Promises, which makes it easier to work with
asynchronous code.
fetch('https://api.example.com/data')
.then(response => {
// Check if the request was successful
if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status}`);
}
// Parse the response as JSON
return response.json();
})
.then(data => {
// Work with the data
console.log('Data:', data);
})
.catch(error => {
// Handle errors
console.error('Fetch error:', error);
});
if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status}`);
}
The Fetch API uses Request and Response objects to represent HTTP requests and
responses.
The Response object represents the response to a request. It has several useful
properties and methods:
fetch('https://api.example.com/data')
.then(response => {
// Response properties
console.log('Status:', response.status);
console.log('Status text:', response.statusText);
console.log('OK?', response.ok);
console.log('Content type:',
response.headers.get('content-type'));
console.log('Is redirected?', response.redirected);
The Response object provides methods for handling different data formats:
// JSON data
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log('JSON data:', data));
// Text data
fetch('https://example.com/page.html')
.then(response => response.text())
.then(text => console.log('Text data:', text));
// FormData
fetch('https://example.com/form-data')
.then(response => response.formData())
.then(formData => {
console.log('Form field value:', formData.get('field-
name'));
});
const data = {
name: 'John Doe',
email: '[email protected]',
message: 'Hello, world!'
};
fetch('https://api.example.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
console.log('Success:', result);
})
.catch(error => {
console.error('Error:', error);
});
Uploading Files
fetch('https://api.example.com/data', {
// HTTP method
method: 'GET', // 'POST', 'PUT', 'DELETE', etc.
// Headers
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
// Request body
body: JSON.stringify({ key: 'value' }), // Only for POST,
PUT, etc.
// Credentials
credentials: 'same-origin', // 'omit', 'same-origin',
'include'
// Cache mode
cache: 'default', // 'no-cache', 'reload', 'force-cache',
'only-if-cached'
// Redirect mode
redirect: 'follow', // 'error', 'manual'
// Referrer
referrer: 'client', // or a URL
// Referrer policy
referrerPolicy: 'no-referrer-when-downgrade', // Various
options available
// Integrity
integrity: 'sha256-hash', // Subresource integrity check
// Keep-alive
keepalive: false, // true to allow the request to outlive
the page
Fetch doesn't reject on HTTP error status. It only rejects on network failures or if
something prevented the request from completing. You need to check response.ok or
response.status :
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
// Create an error with the status text
throw new Error(`HTTP error! Status: $
{response.status} ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Data:', data);
})
.catch(error => {
console.error('Fetch error:', error);
});
if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status} ${response.statusText}`);
}
Fetch doesn't have a built-in timeout option, but you can implement one using
AbortController:
throw error;
});
}
// Usage
fetchWithTimeout('https://api.example.com/data', {}, 3000)
.then(response => response.json())
.then(data => console.log('Data:', data))
.catch(error => console.error('Error:', error));
Canceling Requests
// Create an AbortController
const controller = new AbortController();
const signal = controller.signal;
Understanding CORS
The same-origin policy prevents a malicious site from reading sensitive data from
another site. However, there are legitimate cases where cross-origin requests are
needed. CORS enables servers to specify who can access their resources and how.
CORS Headers
Simple Requests
A request is "simple" if it meets all these conditions: - Uses GET, HEAD, or POST method -
Only uses CORS-safe headers - If it's a POST request with a body, the Content-Type is one
of: - application/x-www-form-urlencoded - multipart/form-data - text/plain - No event
listeners are registered on any XMLHttpRequestUpload object - No ReadableStream
object is used
Simple requests are sent directly to the server with an Origin header.
Preflighted Requests
Requests that don't meet the criteria for simple requests are "preflighted." The browser
first sends an OPTIONS request to check if the actual request is safe to send.
There's not much you can do on the client side to bypass CORS restrictions, as they're
enforced by the browser. However, you can configure your fetch requests to include
credentials:
fetch('https://api.example.com/data', {
credentials: 'include' // Includes cookies in the request
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
CORS Workarounds
If you don't control the server and it doesn't support CORS, you have a few options:
1. Proxy Server: Set up a server on your domain that forwards requests to the target
API
2. JSONP: An older technique that uses script tags to bypass same-origin policy
(limited to GET requests)
3. Server-Side Requests: Make the request from your server instead of the client
app.listen(3000, () => {
console.log('Proxy server running on port 3000');
});
// Client-side code
fetch('/api/proxy?url=https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
class ApiClient {
constructor(baseUrl, options = {}) {
this.baseUrl = baseUrl;
this.defaultOptions = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
credentials: options.credentials || 'same-origin',
mode: options.mode || 'cors',
timeout: options.timeout || 10000
};
}
// Set up timeout
const timeout = options.timeout ||
this.defaultOptions.timeout;
const timeoutId = setTimeout(() => controller.abort(),
timeout);
try {
// Merge options
const fetchOptions = {
...this.defaultOptions,
...options,
headers: {
...this.defaultOptions.headers,
...options.headers
},
signal
};
throw error;
}
}
// Usage example
const api = new ApiClient('https://api.example.com');
// Example usage
async function main() {
try {
await login('user123', 'password123');
const userData = await getUserData(123);
console.log('User data:', userData);
main();
10.6 Chapter Summary and Exercises
Summary
AJAX and the Fetch API are essential tools for modern web development. They allow you
to create dynamic, responsive web applications that can communicate with servers
without full page reloads. Understanding these technologies is crucial for building
interactive web experiences.
Exercises
1. Create a function that uses XMLHttpRequest to fetch data from a public API (like
JSONPlaceholder or OpenWeatherMap).
2. Modify your function to handle different response types (JSON, text, etc.).
3. Add error handling to your function.
4. Create a function that makes a POST request with XMLHttpRequest.
5. Implement a progress indicator for a large file download using XMLHttpRequest.
1. Rewrite your XMLHttpRequest function from Exercise 1 using the Fetch API.
2. Create a function that fetches data from multiple endpoints and combines the
results.
3. Implement error handling for your fetch requests.
4. Create a function that uploads a file using the Fetch API.
5. Implement a timeout for fetch requests using AbortController.
Exercise 3: Working with Forms
1. Create a form that submits data using AJAX instead of a traditional form
submission.
2. Implement form validation before submission.
3. Display success or error messages after submission.
4. Create a form that can upload multiple files with progress indicators.
5. Implement a live search feature that fetches results as the user types (with
debouncing).
1. Create a simple client for a public API (like GitHub, Twitter, or a weather API).
2. Implement authentication using headers or tokens.
3. Create functions for different API endpoints (GET, POST, PUT, DELETE).
4. Handle rate limiting and other API-specific requirements.
5. Implement caching for API responses to reduce the number of requests.
1. Fetches the initial list of todos from a server (you can use JSONPlaceholder or
create a mock API).
2. Allows adding new todos (POST request).
3. Allows marking todos as complete (PUT/PATCH request).
4. Allows deleting todos (DELETE request).
5. Handles errors gracefully with user feedback.
6. Implements optimistic updates (update the UI before the server confirms the
change).
7. Includes loading indicators for all asynchronous operations.
By completing these exercises, you'll gain practical experience with AJAX and the Fetch
API, which are essential skills for modern web development. These techniques will allow
you to create more dynamic and responsive web applications.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<title>Interactive To-Do List</title>
<style>
body { font-family: sans-serif; }
ul { list-style: none; padding: 0; }
li { display: flex; align-items: center; margin-bottom:
5px; }
li.completed span { text-decoration: line-through;
color: grey; }
button { margin-left: 10px; }
</style>
</head>
<body>
<h1>My To-Do List</h1>
<input type="text" id="new-todo" placeholder="Add a new
task">
<button id="add-btn">Add</button>
<ul id="todo-list"></ul>
<script>
(function() {
const todoList = document.getElementById("todo-
list");
const newTodoInput = document.getElementById("new-
todo");
const addBtn = document.getElementById("add-btn");
const checkbox =
document.createElement("input");
checkbox.type = "checkbox";
checkbox.classList.add("toggle-complete");
const deleteBtn =
document.createElement("button");
deleteBtn.textContent = "Delete";
deleteBtn.classList.add("delete-btn");
li.appendChild(checkbox);
li.appendChild(span);
li.appendChild(deleteBtn);
return li;
}
if (!listItem)
return; // Clicked outside a list item
class SimpleApiClient {
constructor(baseUrl) {
if (!baseUrl) {
throw new Error("Base URL is required for API
Client");
}
this.baseUrl = baseUrl;
}
const fetchOptions = {
method: options.method || "GET",
headers: {
"Content-Type": "application/json",
...options.headers,
},
signal,
...options,
};
try {
const response = await fetch(url, fetchOptions);
clearTimeout(timeoutId);
if (!response.ok) {
let errorData = {};
try {
errorData = await response.json();
} catch (e) {
// Ignore if response is not JSON
}
throw new Error(errorData.message || `HTTP
error! Status: ${response.status}`);
}
// Public methods
get(endpoint, options = {}) {
return this.#request(endpoint, { ...options, method:
"GET" });
}
} catch (error) {
console.error("API Client Test Failed:", error);
}
}
testApiClient();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<title>Debounced Search</title>
</head>
<body>
<h1>Search Wikipedia</h1>
<input type="text" id="search-input" placeholder="Type to
search...">
<ul id="results-list"></ul>
<script>
// Debounce function
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
resultsList.innerHTML = "<li>Loading...</li>";
try {
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status}`);
}
const data = await response.json();
displayResults(data);
} catch (error) {
console.error("Search failed:", error);
resultsList.innerHTML = "<li>Error loading
results.</li>";
}
}
if (titles.length === 0) {
resultsList.innerHTML = "<li>No results found.</
li>";
return;
}
Concepts Used: - Events: input event listener - Closures: The debounce function
returns a closure that remembers timeoutId . - DOM Manipulation: Getting input value,
updating innerHTML of the results list. - Fetch API: Making requests to the Wikipedia
API. - Promises: async / await for handling the fetch request. - Error Handling: try /
catch for fetch errors.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<title>Image Gallery</title>
<style>
.gallery { display: flex; flex-wrap: wrap; gap: 10px; }
.gallery img { width: 150px; height: 100px; object-fit:
cover; cursor: pointer; }
.lightbox {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1000; /* Sit on top */
left: 0; top: 0;
width: 100%; height: 100%;
overflow: auto; /* Enable scroll if needed */
background-color: rgba(0,0,0,0.8); /* Black w/
opacity */
justify-content: center;
align-items: center;
}
.lightbox-content {
margin: auto;
display: block;
max-width: 80%;
max-height: 80%;
}
.lightbox-close {
position: absolute;
top: 15px; right: 35px;
color: #f1f1f1;
font-size: 40px;
font-weight: bold;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Image Gallery</h1>
<div class="gallery" id="image-gallery">
<!-- Images will be loaded here -->
</div>
<script>
(function() {
const galleryContainer =
document.getElementById("image-gallery");
const lightbox =
document.getElementById("myLightbox");
const lightboxImage =
document.getElementById("lightboxImage");
const closeBtn = document.querySelector(".lightbox-
close");
})();
</script>
</body>
</html>
Concepts Used: - DOM Manipulation: Creating and appending img elements, setting
src and dataset attributes, manipulating style.display . - Events: click ,
keydown , event delegation. - Data Attributes: Using dataset to store the full image
URL. - Closures: IIFE for encapsulation, event handlers close over lightbox ,
lightboxImage , etc. - Basic CSS for styling the gallery and lightbox.
Example 5: Custom Event Emitter
This example demonstrates creating a simple event emitter class using advanced object
features and closures.
class EventEmitter {
constructor() {
this._events = {}; // Use an underscore convention for
"private"
}
// Emit an event
emit(eventName, ...args) {
if (!this._events[eventName]) {
return;
}
// Call each listener with the provided arguments
this._events[eventName].forEach(listener => {
try {
listener.apply(this, args);
} catch (error) {
console.error(`Error in listener for event "$
{eventName}":`, error);
}
});
}
function handleData(data) {
console.log("Data received:", data);
}
function handleUrgent(message) {
console.warn("Urgent message:", message);
}
function handleOnce(payload) {
console.log("This will only run once:", payload);
}
// Register listeners
emitter.on("data", handleData);
emitter.on("urgent", handleUrgent);
emitter.once("special", handleOnce);
// Emit events
console.log("Emitting 'data' event...");
emitter.emit("data", { id: 1, value: "Test" });
// Remove a listener
console.log("\nRemoving 'data' listener...");
emitter.off("data", handleData);
Concepts Used: - Advanced Objects: ES6 Class, methods, this keyword. - Data
Structures: Using an object ( _events ) to store arrays of listeners. - Closures: The
onceWrapper function forms a closure over the original listener and eventName . -
Spread Syntax ( ...args ): Handling variable numbers of arguments for listeners. -
apply() : Calling listener functions with the correct this context and arguments. -
Error Handling: Basic try / catch within the emit method to prevent one listener
error from stopping others.
These examples illustrate how the intermediate JavaScript concepts work together in
practical scenarios. Experimenting with and modifying these examples will further
solidify your understanding.
The Singleton pattern ensures that a class has only one instance and provides a global
point of access to it.
function createInstance() {
// Private variables and methods
const privateVariable = "I am private";
function privateMethod() {
console.log("Private method called");
}
return {
// Public variables and methods
publicVariable: "I am public",
publicMethod: function() {
console.log("Public method called");
privateMethod();
},
getPrivateVariable: function() {
return privateVariable;
}
};
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// Usage
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
instance1.publicMethod();
// Output:
// Public method called
// Private method called
class SingletonClass {
static instance = null;
#privateData = "Secret";
constructor() {
if (SingletonClass.instance) {
return SingletonClass.instance;
}
SingletonClass.instance = this;
console.log("Singleton instance created.");
}
getData() {
return `Data: ${this.#privateData}`;
}
static getInstance() {
if (!SingletonClass.instance) {
SingletonClass.instance = new SingletonClass();
}
return SingletonClass.instance;
}
}
The Factory pattern provides an interface for creating objects in a superclass, but allows
subclasses to alter the type of objects that will be created.
Use Case: Creating different types of objects based on input parameters without
exposing the creation logic to the client.
Employee.prototype.say = function() {
console.log(`I am ${this.name} and I am a ${this.type}`);
};
// Factory
function EmployeeFactory() {
this.create = function(name, type) {
switch (type) {
case 1: // Developer
return new Employee(name, "Developer");
case 2: // Tester
return new Employee(name, "Tester");
default:
throw new Error("Invalid employee type");
}
};
}
// Usage
const factory = new EmployeeFactory();
const employees = [];
class Employee {
constructor(name, role) {
this.name = name;
this.role = role;
}
introduce() {
console.log(`Hi, I'm ${this.name}, a ${this.role}.`);
}
}
class EmployeeFactory {
static create(name, type) {
switch (type.toLowerCase()) {
case "developer":
return new Developer(name);
case "tester":
return new Tester(name);
default:
throw new Error("Invalid employee type
specified.");
}
}
}
// Usage
const dev = EmployeeFactory.create("Alice", "developer");
const tester = EmployeeFactory.create("Bob", "tester");
This is the most basic pattern for creating objects using constructor functions or classes.
Use Case: Creating multiple instances of objects with similar properties and methods.
// Constructor Function
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
console.log(`Hello, my name is ${this.name} and I am $
{this.age}.`);
};
}
// ES6 Class
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
}
displayInfo() {
console.log(`Car: ${this.make} ${this.model}`);
}
}
We covered the Module Pattern extensively in Chapter 9. It uses closures to create private
and public members, encapsulating functionality.
Use Case: Organizing code into logical units, creating private state, and avoiding global
namespace pollution.
CalculatorModule.add(10);
CalculatorModule.subtract(3);
console.log(CalculatorModule.getResult()); // 7
// console.log(CalculatorModule.result); // undefined (private)
// Complex Subsystem
class SubsystemA {
operationA() { console.log("Subsystem A operation"); }
}
class SubsystemB {
operationB() { console.log("Subsystem B operation"); }
}
class SubsystemC {
operationC() { console.log("Subsystem C operation"); }
}
// Facade
class Facade {
constructor() {
this.subsystemA = new SubsystemA();
this.subsystemB = new SubsystemB();
this.subsystemC = new SubsystemC();
}
// Simplified interface
performComplexOperation() {
console.log("Facade performing complex operation:");
this.subsystemA.operationA();
this.subsystemB.operationB();
this.subsystemC.operationC();
}
performSimpleOperationA() {
console.log("Facade performing simple operation A:");
this.subsystemA.operationA();
}
}
// Usage
const facade = new Facade();
facade.performComplexOperation();
console.log("---");
facade.performSimpleOperationA();
// Output:
// Facade performing complex operation:
// Subsystem A operation
// Subsystem B operation
// Subsystem C operation
// ---
// Facade performing simple operation A:
// Subsystem A operation
// Base component
class Coffee {
cost() {
return 5;
}
description() {
return "Simple Coffee";
}
}
// Decorator 1: Milk
class MilkDecorator {
constructor(coffee) {
this._coffee = coffee;
}
cost() {
return this._coffee.cost() + 2;
}
description() {
return this._coffee.description() + ", Milk";
}
}
// Decorator 2: Sugar
class SugarDecorator {
constructor(coffee) {
this._coffee = coffee;
}
cost() {
return this._coffee.cost() + 1;
}
description() {
return this._coffee.description() + ", Sugar";
}
}
// Usage
let myCoffee = new Coffee();
console.log(myCoffee.description(), "Cost:",
myCoffee.cost()); // Simple Coffee Cost: 5
Use Case: Integrating a new library with an existing system that expects a different
interface.
// Old Interface (Target)
class OldCalculator {
operations(term1, term2, operation) {
switch (operation) {
case 'add': return term1 + term2;
case 'sub': return term1 - term2;
default: return NaN;
}
}
}
// Adapter
class CalculatorAdapter {
constructor() {
this.newCalculator = new NewCalculator();
}
// Usage
const oldCalc = new OldCalculator();
console.log("Old Calc Add:", oldCalc.operations(10, 5,
'add')); // 15
The Observer pattern defines a one-to-many dependency between objects so that when
one object changes state, all its dependents are notified and updated automatically.
// Subject (Observable)
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observerToRemove) {
this.observers = this.observers.filter(observer =>
observer !== observerToRemove);
}
notify(data) {
this.observers.forEach(observer => {
try {
observer.update(data);
} catch (error) {
console.error("Error notifying observer:",
error);
}
});
}
}
// Observer
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} received update:`, data);
}
}
// Usage
const subject = new Subject();
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.subscribe(observer3);
The Command pattern turns a request into a stand-alone object that contains all
information about the request. This lets you parameterize methods with different
requests, delay or queue a request's execution, and support undoable operations.
Use Case: Implementing undo/redo functionality, queuing tasks, decoupling sender and
receiver.
// Receiver
class Calculator {
constructor() {
this.currentValue = 0;
}
operation(operator, value) {
switch (operator) {
case '+': this.currentValue += value; break;
case '-': this.currentValue -= value; break;
case '*': this.currentValue *= value; break;
case '/': this.currentValue /= value; break;
default: throw new Error("Invalid operator");
}
console.log(`Current value: ${this.currentValue}`);
}
}
// Invoker
class CommandHistory {
constructor() {
this.commands = [];
this.redoStack = [];
}
execute(command) {
command.execute();
this.commands.push(command);
this.redoStack = []; // Clear redo stack on new command
}
undo() {
const command = this.commands.pop();
if (command) {
command.undo();
this.redoStack.push(command);
}
}
redo() {
const command = this.redoStack.pop();
if (command) {
command.execute();
this.commands.push(command);
}
}
}
// Usage
const calculator = new Calculator();
const history = new CommandHistory();
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes
them interchangeable. Strategy lets the algorithm vary independently from clients that
use it.
// Context
class ShippingCalculator {
constructor() {
this.strategy = null;
}
setStrategy(strategy) {
this.strategy = strategy;
}
calculate(packageDetails) {
if (!this.strategy) {
throw new Error("Shipping strategy not set.");
}
return this.strategy.calculate(packageDetails);
}
}
// Concrete Strategies
class FedexStrategy extends ShippingStrategy {
calculate(packageDetails) {
// Fedex calculation logic based on weight, dimensions,
etc.
console.log("Calculating using Fedex strategy...");
return packageDetails.weight * 2.5 + 5; // Example
calculation
}
}
// Usage
const calculator = new ShippingCalculator();
const packageInfo = { weight: 10, from: "NY", to: "CA" };
calculator.setStrategy(new FedexStrategy());
console.log("Fedex Cost:",
calculator.calculate(packageInfo)); // 30
calculator.setStrategy(new UPSStrategy());
console.log("UPS Cost:",
calculator.calculate(packageInfo)); // 29
calculator.setStrategy(new USPSStrategy());
console.log("USPS Cost:",
calculator.calculate(packageInfo)); // 21
12.5 Anti-Patterns
While design patterns offer solutions, anti-patterns describe common pitfalls or
ineffective solutions to problems.
• Global Variables: Polluting the global namespace, leading to naming conflicts and
hard-to-debug code.
• Callback Hell: Deeply nested callbacks, making code unreadable and difficult to
maintain (often solved by Promises or async/await).
• Magic Strings/Numbers: Using unnamed string or number literals instead of
constants, making code hard to understand and refactor.
• God Object: A single object that knows or does too much, violating the single
responsibility principle.
• Spaghetti Code: Code with complex and tangled control flow structure, especially
using many GOTO-like jumps or excessive global state modification.
Summary
Design patterns provide valuable templates for solving common software design
problems. Applying them appropriately can significantly improve the quality,
maintainability, and scalability of your JavaScript applications. However, it's crucial to
understand the context and trade-offs before applying a pattern; overusing or misusing
patterns can lead to unnecessary complexity.
Exercises
Create a Validator class that can use different validation strategies (e.g.,
EmailStrategy , PasswordStrategy , NumberStrategy ). Implement the Strategy
pattern to allow the Validator to switch strategies and validate input data
accordingly.
Implement a simple text editor functionality using the Command pattern. Create
commands for actions like InsertTextCommand , DeleteTextCommand . Use a
command history to implement undo and redo functionality for these text operations.
Before ES6 modules, developers used Immediately Invoked Function Expressions (IIFEs)
to create modules:
function increment() {
count++;
}
function decrement() {
count--;
}
// Public API
return {
increment: increment,
decrement: decrement,
getCount: function() {
return count;
},
reset: function() {
count = 0;
}
};
})();
// Usage
counterModule.increment();
counterModule.increment();
console.log(counterModule.getCount()); // 2
counterModule.reset();
console.log(counterModule.getCount()); // 0
function subtract(a, b) {
return a - b;
}
console.log(math.add(5, 3)); // 8
console.log(math.subtract(10, 4)); // 6
console.log(math.multiply(2, 3)); // 6
13.2.3 AMD (Asynchronous Module Definition)
// Usage
require(['myModule'], function(myModule) {
myModule.init();
});
UMD is a pattern that attempts to offer compatibility with multiple module systems:
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory(require('jquery'));
} else {
// Browser globals
root.myModule = factory(root.jQuery);
}
}(typeof self !== 'undefined' ? self : this, function($) {
// Module code here
function myMethod() {
return 'Hello World';
}
// math.js
export function add(a, b) {
return a + b;
}
// Default export
export default function multiply(a, b) {
return a * b;
}
// main.js
// Named imports
import { add, subtract, PI } from './math.js';
// Default import
import multiply from './math.js';
// Rename imports
import { add as addition } from './math.js';
console.log(add(5, 3)); // 8
console.log(subtract(10, 4)); // 6
console.log(multiply(2, 3)); // 6
console.log(addition(2, 2)); // 4
console.log(math.add(1, 1)); // 2
console.log(math.PI); // 3.14159
// Named exports
export const name = 'John';
export function sayHello() {
console.log(`Hello, ${name}!`);
}
greet() {
console.log(`Hi, I'm ${this.name}`);
}
}
// module1.js
console.log('Module 1 is being evaluated');
export const value = 42;
// module2.js
import { value } from './module1.js';
console.log('In module2, value =', value);
export function getValue() {
return value;
}
// main.js
import { value } from './module1.js'; // Module 1 is evaluated
only once
import { getValue } from './module2.js';
console.log('In main, value =', value); // 42
console.log('getValue() =', getValue()); // 42
Webpack
Webpack is one of the most widely used bundlers, known for its flexibility and extensive
plugin ecosystem:
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader']
}
]
},
plugins: [
// Various plugins can be added here
]
};
Rollup
Rollup specializes in ES module bundling and is known for its tree-shaking capabilities:
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import { terser } from 'rollup-plugin-terser';
export default {
input: 'src/main.js',
output: {
file: 'dist/bundle.js',
format: 'iife', // Immediately-invoked function
expression
sourcemap: true
},
plugins: [
resolve(), // Locate modules using the Node resolution
algorithm
commonjs(), // Convert CommonJS modules to ES modules
babel({ babelHelpers: 'bundled' }),
terser() // Minify the bundle
]
};
Parcel
# Bundle an application
parcel index.html
esbuild
// esbuild.config.js
const esbuild = require('esbuild');
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
minify: true,
sourcemap: true,
target: ['chrome58', 'firefox57', 'safari11', 'edge16'],
outfile: 'dist/bundle.js'
}).catch(() => process.exit(1));
Vite
Vite is a modern build tool that leverages native ES modules during development:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
Code Splitting
Code splitting allows you to split your bundle into smaller chunks that can be loaded on
demand:
// Webpack code splitting example
import React from 'react';
function App() {
return (
<div>
<React.Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</React.Suspense>
</div>
);
}
Tree Shaking
// utils.js
export function used() {
console.log('This function is used');
}
// main.js
import { used } from './utils';
used(); // Only the 'used' function will be included in the
bundle
# Initialize npm
npm init -y
my-js-project/
├── dist/ # Output directory (generated)
├── src/ # Source code
│ ├── components/ # Component modules
│ ├── styles/ # CSS files
│ ├── utils/ # Utility modules
│ └── index.js # Entry point
├── .babelrc # Babel configuration
├── package.json # Project metadata and dependencies
└── webpack.config.js # Webpack configuration
webpack.config.js
module.exports = {
mode: process.env.NODE_ENV || 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[contenthash].js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
devServer: {
static: {
directory: path.join(__dirname, 'dist')
},
compress: true,
port: 9000,
hot: true
}
};
.babelrc
{
"presets": [
["@babel/preset-env", {
"targets": "> 0.25%, not dead"
}]
]
}
package.json Scripts
{
"scripts": {
"start": "webpack serve",
"build": "NODE_ENV=production webpack",
"build:dev": "webpack"
}
}
src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<title>Modern JS Project</title>
</head>
<body>
<div id="app"></div>
<!-- No script tag needed; HtmlWebpackPlugin will inject it
-->
</body>
</html>
src/utils/math.js
increment() {
this.count++;
this.render();
}
decrement() {
this.count--;
this.render();
}
render() {
this.element.innerHTML = `
<div class="counter">
<h2>Counter: ${this.count}</h2>
<button class="decrement">-</button>
<button class="increment">+</button>
</div>
`;
this.element.querySelector('.increment').addEventListener('click',
() => this.increment());
this.element.querySelector('.decrement').addEventListener('click',
() => this.decrement());
}
getElement() {
return this.element;
}
}
src/styles/main.css
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
}
.counter {
border: 1px solid #ccc;
padding: 20px;
border-radius: 5px;
max-width: 300px;
margin: 0 auto;
text-align: center;
}
button {
padding: 10px 15px;
margin: 0 5px;
font-size: 16px;
cursor: pointer;
}
src/index.js
import './styles/main.css';
import Counter from './components/Counter';
import { add } from './utils/math';
document.addEventListener('DOMContentLoaded', () => {
const app = document.getElementById('app');
// Create a heading
const heading = document.createElement('h1');
heading.textContent = 'Modern JavaScript Project';
app.appendChild(heading);
Circular dependencies occur when module A imports from module B, and module B
imports from module A:
// moduleA.js
import { functionB } from './moduleB.js';
// moduleB.js
import { functionA } from './moduleA.js';
// main.js
import { functionA } from './moduleA.js';
functionA();
Circular dependencies can lead to unexpected behavior and should generally be avoided
by restructuring your code.
// original-module.js
export const name = 'Original Module';
export function greet() {
console.log(`Hello from ${name}`);
}
// augmented-module.js
export * from './original-module.js';
export function additionalFunction() {
console.log('This is an additional function');
}
// main.js
import { greet, additionalFunction } from './augmented-
module.js';
greet(); // "Hello from Original Module"
additionalFunction(); // "This is an additional function"
Modules can have side effects (code that executes when the module is loaded):
// side-effect.js
console.log('This module has a side effect');
document.body.style.backgroundColor = 'lightblue';
// main.js
import './side-effect.js'; // The side effect runs when imported
// singleton.js
let instance = null;
class Singleton {
constructor() {
if (instance) {
return instance;
}
instance = this;
this.data = [];
}
add(item) {
this.data.push(item);
}
get items() {
return [...this.data];
}
}
// main.js
import singleton from './singleton.js';
singleton.add('item 1');
// another-file.js
import singleton from './singleton.js'; // Same instance
singleton.add('item 2');
console.log(singleton.items); // ['item 1', 'item 2']
Summary
• Module Patterns: From IIFEs to CommonJS, AMD, UMD, and ES6 modules
• ES6 Modules: The modern standard with import/export syntax
• Module Bundlers: Tools like Webpack, Rollup, Parcel, esbuild, and Vite
• Project Setup: How to set up a modern JavaScript project with modules and
bundling
• Advanced Techniques: Circular dependencies, module augmentation, and side
effects
Exercises
1. Create a module math.js that exports functions for basic arithmetic operations
(add, subtract, multiply, divide).
2. Create a module utils.js that exports functions for string manipulation
(capitalize, reverse, countWords).
3. Create a main module that imports and uses functions from both modules.
Synchronous code executes line by line, with each operation completing before the
next one begins:
console.log("First");
console.log("Second");
console.log("Third");
// Output:
// First
// Second
// Third
Asynchronous code allows operations to run in the background while the rest of the
code continues to execute:
console.log("First");
setTimeout(() => {
console.log("Second (after delay)");
}, 1000);
console.log("Third");
// Output:
// First
// Third
// Second (after delay) - appears after 1 second
function fetchData(callback) {
// Simulate an API call with setTimeout
setTimeout(() => {
const data = { id: 1, name: "John Doe" };
callback(null, data); // First parameter is error (null
means no error)
}, 1000);
}
console.log("Fetching data...");
// Output:
// Fetching data...
// Data: { id: 1, name: "John Doe" } - appears after 1 second
Once a Promise is fulfilled or rejected, it's considered "settled" and cannot change state
again.
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
// Asynchronous operation
const success = true;
if (success) {
resolve("Operation succeeded!"); // Fulfilled
} else {
reject("Operation failed!"); // Rejected
}
});
// Using a Promise
myPromise
.then(result => {
console.log("Success:", result);
})
.catch(error => {
console.error("Error:", error);
})
.finally(() => {
console.log("Promise settled (fulfilled or rejected)");
});
One of the key advantages of Promises is the ability to chain them, making sequential
asynchronous operations more readable:
function fetchUser(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: "User " + userId });
} else {
reject("Invalid user ID");
}
}, 1000);
});
}
function fetchPosts(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: 1, userId: userId, title: "Post 1" },
{ id: 2, userId: userId, title: "Post 2" }
]);
}, 1000);
});
}
function fetchComments(postId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: 1, postId: postId, text: "Comment 1" },
{ id: 2, postId: postId, text: "Comment 2" }
]);
}, 1000);
});
}
// Chain promises
fetchUser(1)
.then(user => {
console.log("User:", user);
return fetchPosts(user.id); // Return a new Promise
})
.then(posts => {
console.log("Posts:", posts);
return fetchComments(posts[0].id); // Return a new
Promise
})
.then(comments => {
console.log("Comments:", comments);
})
.catch(error => {
console.error("Error in chain:", error);
});
14.2.3 Promise Methods
Promise.all()
Promise.all() takes an array of Promises and returns a new Promise that fulfills
when all input Promises fulfill, or rejects if any input Promise rejects:
Promise.race()
Promise.race() returns a Promise that fulfills or rejects as soon as one of the input
Promises fulfills or rejects:
Promise.race([promise1, promise2])
.then(value => {
console.log("Fastest promise won:", value); // "Fast"
})
.catch(error => {
console.error("Fastest promise rejected:", error);
});
Promise.allSettled()
Promise.any()
fetchData()
.then(data => {
try {
const processedData = processData(data);
return processedData;
} catch (error) {
console.error("Error processing data:", error);
// Return a default value to continue the chain
return { default: true };
}
})
.then(processedData => {
displayData(processedData);
})
.catch(error => {
// This catches any other errors
console.error("Unhandled error:", error);
});
14.3 Async/Await
Async/await, introduced in ES2017, is syntactic sugar built on top of Promises. It makes
asynchronous code look and behave more like synchronous code, which is often easier
to understand.
14.3.1 Basic Syntax
// Equivalent to:
function fetchUserDataPromise() {
return Promise.resolve({ id: 1, name: "John Doe" });
}
The await keyword can only be used inside an async function. It pauses the
execution of the function until the Promise is settled:
displayUserData();
While sequential execution is straightforward with async/await, you can also run
operations in parallel:
Error handling with async/await is more intuitive than with Promise chains, as you can
use traditional try/catch blocks:
Top-level await
Prior to ES2022, await could only be used inside an async function. Now, top-level
await is supported in ES modules:
// In an ES module
const data = await fetch('https://api.example.com/data').then(r
=> r.json());
console.log(data);
Error Propagation
For code clarity, it's generally best to stick with one style within a function:
// Using Promises
function fetchUserData(userId) {
return fetch(`https://jsonplaceholder.typicode.com/users/$
{userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status}`);
}
return response.json();
})
.then(user => {
return fetch(`https://jsonplaceholder.typicode.com/
posts?userId=${user.id}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status}`);
}
return response.json();
})
.then(posts => {
return { user, posts };
});
});
}
// Using async/await
async function fetchUserDataAsync(userId) {
// Fetch user
const userResponse = await fetch(`https://
jsonplaceholder.typicode.com/users/${userId}`);
if (!userResponse.ok) {
throw new Error(`HTTP error! Status: $
{userResponse.status}`);
}
const user = await userResponse.json();
// Fetch posts
const postsResponse = await fetch(`https://
jsonplaceholder.typicode.com/posts?userId=${user.id}`);
if (!postsResponse.ok) {
throw new Error(`HTTP error! Status: $
{postsResponse.status}`);
}
const posts = await postsResponse.json();
// Usage
fetchUserDataAsync(1)
.then(data => {
console.log("User:", data.user.name);
console.log("Posts:", data.posts.length);
})
.catch(error => {
console.error("Error:", error);
});
14.4.2 Implementing Retry Logic
// Usage
fetchWithRetry('https://api.example.com/data')
.then(data => console.log("Data:", data))
.catch(error => console.error("Failed after retries:",
error));
// Usage
fetchWithTimeout('https://api.example.com/data', {}, 3000)
.then(response => response.json())
.then(data => console.log("Data:", data))
.catch(error => console.error("Error:", error));
return results;
}
// Helper function
async function processItem(item) {
// Simulate processing time
await new Promise(resolve => setTimeout(resolve, 1000));
return `Processed ${item}`;
}
// Usage comparison
async function comparePerformance() {
const items = [1, 2, 3, 4, 5];
console.time('Sequential');
await processItemsSequentially(items);
console.timeEnd('Sequential'); // ~5000ms
console.time('Concurrent');
await processItemsConcurrently(items);
console.timeEnd('Concurrent'); // ~1000ms
}
comparePerformance();
Sometimes you need to limit concurrency while still processing items in parallel:
class PromiseQueue {
constructor(concurrency = 3) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
add(promiseFactory) {
return new Promise((resolve, reject) => {
this.queue.push({ promiseFactory, resolve,
reject });
this.next();
});
}
next() {
if (this.running >= this.concurrency ||
this.queue.length === 0) {
return;
}
promiseFactory()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
this.next();
});
}
return results;
}
}
// Usage
async function testQueue() {
const queue = new PromiseQueue(2); // Process 2 items at a
time
const items = [1, 2, 3, 4, 5, 6, 7, 8];
console.time('Queue Processing');
const results = await queue.processAll(items, processItem);
console.timeEnd('Queue Processing');
console.log("Results:", results);
}
testQueue();
// Usage
const { promise, cancel } = fetchWithCancellation('https://
api.example.com/data');
// Set up a timeout to cancel the request after 2 seconds
setTimeout(() => {
console.log("Cancelling request...");
cancel();
}, 2000);
promise
.then(data => console.log("Data:", data))
.catch(error => {
if (error.name === 'AbortError') {
console.log("Request was cancelled");
} else {
console.error("Error:", error);
}
});
function memoizePromise(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Cache hit!");
return cache.get(key);
}
console.log("Cache miss!");
const promise = fn(...args).catch(error => {
// Remove failed promises from cache
cache.delete(key);
throw error;
});
cache.set(key, promise);
return promise;
};
}
// Usage
const fetchUserMemoized = memoizePromise(async (userId) => {
const response = await fetch(`https://
jsonplaceholder.typicode.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}
`);
}
return response.json();
});
JavaScript doesn't provide a built-in way to inspect a Promise's state, but we can create
a utility for this:
function inspectPromise(promise) {
let state = "pending";
let result;
return inspection;
}
// Usage
async function testInspection() {
const promise1 = Promise.resolve("Success!");
const promise2 = Promise.reject("Failure!");
const promise3 = new Promise(resolve => setTimeout(() =>
resolve("Delayed"), 1000));
testInspection();
Summary
• Callbacks: The traditional approach with limitations like callback hell and complex
error handling
• Promises: Objects representing the eventual completion or failure of an
asynchronous operation
• Promise states: pending, fulfilled, rejected
• Promise methods: then() , catch() , finally()
• Static methods: Promise.all() , Promise.race() , Promise.allSettled() ,
Promise.any()
• Async/Await: Syntactic sugar built on top of Promises
• async functions that return Promises
• await for pausing execution until a Promise settles
• Error handling with try/catch blocks
• Real-World Examples: Practical applications of Promises and async/await
• Fetching data from APIs
• Implementing retry logic and timeouts
• Sequential vs. concurrent operations
• Building a Promise queue
• Advanced Patterns: Sophisticated techniques for working with Promises
• Cancellable Promises with AbortController
• Promise memoization
• Promise state inspection
Exercises
1. Create a function delay(ms) that returns a Promise that resolves after the
specified number of milliseconds.
2. Use the delay function to create a sequence of timed operations (e.g., display
messages at 1-second intervals).
3. Implement a function randomDelay() that resolves after a random delay
between 1 and 5 seconds.
4. Create a Promise that randomly resolves or rejects based on a 50/50 chance.
5. Chain multiple Promises together with different success and error handlers.
Exercise 3: Async/Await
By completing these exercises, you'll gain practical experience with Promises and async/
await, which are essential for modern JavaScript development.
15.1.1 localStorage
localStorage allows you to store key-value pairs that persist even when the browser
is closed and reopened:
// Storing data
localStorage.setItem('username', 'JohnDoe');
localStorage.setItem('preferences', JSON.stringify({
theme: 'dark',
fontSize: 16,
notifications: true
}));
// Retrieving data
const username = localStorage.getItem('username');
console.log(username); // "JohnDoe"
const preferences =
JSON.parse(localStorage.getItem('preferences'));
console.log(preferences.theme); // "dark"
15.1.2 sessionStorage
// Retrieving data
const currentPage = sessionStorage.getItem('currentPage');
console.log(currentPage); // "5"
const results =
JSON.parse(sessionStorage.getItem('searchResults'));
console.log(results[0].title); // "Result 1"
When storage changes in one tab, other tabs can listen for these changes:
// Note: This event is not triggered in the same tab that made
the change
// Open a database
const request = indexedDB.open('MyDatabase', 1);
// Add data
const customer = {
name: 'John Doe',
email: '[email protected]',
age: 35
};
addRequest.onsuccess = () => {
console.log('Customer added, ID:', addRequest.result);
};
// Handle errors
request.onerror = (event) => {
console.error('Database error:', event.target.error);
};
function getCustomerById(id) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);
getRequest.onsuccess = () => {
if (getRequest.result) {
resolve(getRequest.result);
} else {
reject(new Error('Customer not found'));
}
};
getRequest.onerror = () => {
reject(getRequest.error);
};
transaction.oncomplete = () => {
db.close();
};
};
request.onerror = () => {
reject(request.error);
};
});
}
// Usage
getCustomerById(1)
.then(customer => {
console.log('Found customer:', customer);
})
.catch(error => {
console.error('Error:', error);
});
function getAllCustomers() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);
if (cursor) {
// Add the customer to our array
customers.push(cursor.value);
cursorRequest.onerror = () => {
reject(cursorRequest.error);
};
transaction.oncomplete = () => {
db.close();
};
};
request.onerror = () => {
reject(request.error);
};
});
}
function findCustomersByName(name) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);
if (cursor) {
customers.push(cursor.value);
cursor.continue();
} else {
resolve(customers);
}
};
cursorRequest.onerror = () => {
reject(cursorRequest.error);
};
transaction.oncomplete = () => {
db.close();
};
};
request.onerror = () => {
reject(request.error);
};
});
}
// Update a customer
function updateCustomer(id, updates) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);
getRequest.onsuccess = () => {
if (!getRequest.result) {
reject(new Error('Customer not found'));
return;
}
putRequest.onsuccess = () => {
resolve(updatedCustomer);
};
putRequest.onerror = () => {
reject(putRequest.error);
};
};
getRequest.onerror = () => {
reject(getRequest.error);
};
transaction.oncomplete = () => {
db.close();
};
};
request.onerror = () => {
reject(request.error);
};
});
}
// Delete a customer
function deleteCustomer(id) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);
deleteRequest.onsuccess = () => {
resolve(true);
};
deleteRequest.onerror = () => {
reject(deleteRequest.error);
};
transaction.oncomplete = () => {
db.close();
};
};
request.onerror = () => {
reject(request.error);
};
});
}
console.log('Timestamp:', new
Date(position.timestamp));
},
// Error callback
(error) => {
switch (error.code) {
case error.PERMISSION_DENIED:
console.error('User denied the request for
geolocation');
break;
case error.POSITION_UNAVAILABLE:
console.error('Location information is
unavailable');
break;
case error.TIMEOUT:
console.error('The request to get user
location timed out');
break;
case error.UNKNOWN_ERROR:
console.error('An unknown error occurred');
break;
}
},
// Options
{
enableHighAccuracy: true, // Use GPS if available
timeout: 5000, // Time to wait for a
position (ms)
maximumAge: 0 // Don't use a cached
position
}
);
} else {
console.error('Geolocation is not supported by this
browser');
}
let watchId;
function startWatchingPosition() {
if ('geolocation' in navigator) {
watchId = navigator.geolocation.watchPosition(
// Success callback (called whenever position
changes)
(position) => {
console.log('Updated position:');
console.log('Latitude:',
position.coords.latitude);
console.log('Longitude:',
position.coords.longitude);
function stopWatchingPosition() {
if (watchId !== undefined) {
navigator.geolocation.clearWatch(watchId);
console.log('Stopped watching position');
watchId = undefined;
}
}
return distance;
}
// Example usage
const distanceInMeters = calculateDistance(40.7128, -74.0060,
34.0522, -118.2437);
console.log(`Distance: ${(distanceInMeters / 1000).toFixed(2)}
km`);
15.4 Web Workers API
Web Workers allow you to run JavaScript in background threads, separate from the main
execution thread.
// Handle errors
worker.onerror = (error) => {
console.error('Worker error:', error.message);
console.error('Error filename:', error.filename);
console.error('Error line number:', error.lineno);
};
return {
inputSum,
processingTime: new Date().getTime()
};
}
// Handle errors
self.onerror = (error) => {
console.error('Error in worker:', error);
};
For large data like ArrayBuffers, you can transfer ownership instead of copying:
Shared Workers can be accessed by multiple scripts or windows from the same origin:
Main script:
// Send a message
sharedWorker.port.postMessage({
source: 'Window ' + window.name,
message: 'Hello from the main script'
});
// Show a notification
function showNotification(title, options = {}) {
if (Notification.permission === 'granted') {
const notification = new Notification(title, {
body: options.body || '',
icon: options.icon || '/path/to/icon.png',
badge: options.badge || '/path/to/badge.png',
image: options.image || '',
tag: options.tag || '',
data: options.data || {},
requireInteraction: options.requireInteraction
|| false,
renotify: options.renotify || false,
silent: options.silent || false,
...options
});
return notification;
} else {
console.warn('Notification permission not granted');
return null;
}
}
// Example usage
document.getElementById('request-
permission').addEventListener('click', async () => {
const permission = await
requestNotificationPermission();
Service Workers can display notifications even when the page is closed:
Main script:
// Example usage
document.getElementById('send-
notification').addEventListener('click', () => {
sendNotificationViaServiceWorker('Service Worker
Notification', {
body: 'This notification was sent via a Service Worker',
icon: '/images/notification-icon.png',
actions: [
{ action: 'explore', title: 'Explore' },
{ action: 'close', title: 'Close' }
],
data: {
notificationId: 123,
url: window.location.href
}
});
});
// Set options
utterance.lang = options.lang || 'en-US';
utterance.pitch = options.pitch || 1;
utterance.rate = options.rate || 1;
utterance.volume = options.volume || 1;
// Event handlers
utterance.onstart = () => {
console.log('Speech started');
};
utterance.onend = () => {
console.log('Speech ended');
};
utterance.onpause = () => {
console.log('Speech paused');
};
utterance.onresume = () => {
console.log('Speech resumed');
};
return utterance;
}
if (voices.length > 0) {
resolve(voices);
} else {
// Wait for voices to be loaded
synth.onvoiceschanged = () => {
voices = synth.getVoices();
resolve(voices);
};
}
});
}
// Example usage
async function setupSpeechSynthesis() {
const voices = await getVoices();
speak(text, {
voice: selectedVoice,
rate: document.getElementById('rate-
slider').value,
pitch: document.getElementById('pitch-
slider').value
});
});
document.getElementById('resume-
button').addEventListener('click', () => {
if (synth.paused) {
synth.resume();
}
});
document.getElementById('cancel-
button').addEventListener('click', () => {
synth.cancel();
});
}
setupSpeechSynthesis();
} else {
console.error('Speech synthesis not supported');
}
if (SpeechRecognition) {
// Create a speech recognition instance
const recognition = new SpeechRecognition();
// Configure recognition
recognition.continuous = true; // Don't stop when the
user stops speaking
recognition.interimResults = true; // Get interim results
recognition.lang = 'en-US'; // Set language
// Create result containers
let finalTranscript = '';
let interimTranscript = '';
// Handle results
recognition.onresult = (event) => {
interimTranscript = '';
if (event.results[i].isFinal) {
finalTranscript += transcript + ' ';
} else {
interimTranscript += transcript;
}
}
// Update the UI
document.getElementById('final-transcript').textContent
= finalTranscript;
document.getElementById('interim-
transcript').textContent = interimTranscript;
};
// Handle errors
recognition.onerror = (event) => {
console.error('Speech recognition error:', event.error);
};
// Set up UI controls
document.getElementById('start-
recognition').addEventListener('click', () => {
finalTranscript = '';
document.getElementById('final-transcript').textContent
= '';
document.getElementById('interim-
transcript').textContent = '';
recognition.start();
});
document.getElementById('stop-
recognition').addEventListener('click', () => {
recognition.stop();
});
} else {
console.error('Speech recognition not supported');
}
Summary
These APIs extend JavaScript's capabilities beyond the core language, enabling web
applications to interact with various aspects of the browser and device. Understanding
and using these APIs effectively can significantly enhance the functionality and user
experience of your web applications.
Exercises
Exercise 2: IndexedDB
1. Build a "Find My Location" feature that displays the user's current position on a
map.
2. Create a distance calculator that measures the distance between two points on a
map.
3. Implement a location tracker that records the user's path over time.
4. Build a "Places Nearby" feature that finds points of interest near the user's
location.
5. Create a geofencing application that triggers alerts when the user enters or leaves a
defined area.
1. Create a web worker that performs complex calculations without freezing the UI.
2. Implement an image processing application that uses a web worker to apply filters.
3. Build a data analysis tool that processes large datasets in a worker.
4. Create a shared worker that maintains state across multiple tabs.
5. Implement a worker pool that distributes tasks among multiple workers.
Build a progressive web application (PWA) that combines multiple modern APIs:
By completing these exercises, you'll gain practical experience with modern JavaScript
APIs and be well-prepared to build advanced web applications.
Chapter 16: Optimization and
Performance
Performance optimization is a critical aspect of JavaScript development. A well-
optimized application provides a better user experience, reduces server load, and can
even improve search engine rankings. This chapter explores techniques and best
practices for optimizing JavaScript code and improving application performance.
Modern browsers use sophisticated JavaScript engines that compile JavaScript code to
machine code:
1. Parsing: The engine parses the JavaScript code into an Abstract Syntax Tree (AST)
2. Compilation: The AST is compiled into bytecode
3. Optimization: The engine applies various optimizations to the code
4. Execution: The optimized code is executed
Different browsers use different JavaScript engines: - Chrome and Edge: V8 - Firefox:
SpiderMonkey - Safari: JavaScriptCore (Nitro)
// Code to measure
for (let i = 0; i < 1000000; i++) {
// Some operation
}
performance.mark('endOperation');
performance.measure('operationDuration', 'startOperation',
'endOperation');
Using console.time()
console.time('operationTimer');
// Code to measure
for (let i = 0; i < 1000000; i++) {
// Some operation
}
Loops are often a source of performance issues, especially when dealing with large
datasets.
Functions are fundamental to JavaScript, and optimizing them can significantly improve
performance.
// Without memoization
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// With memoization
function memoizedFibonacci() {
const cache = {};
if (n <= 1) {
return n;
}
// Compare performance
console.time('fibonacci-no-memo');
fibonacci(30);
console.timeEnd('fibonacci-no-memo');
console.time('fibonacci-memo');
fibMemo(30);
console.timeEnd('fibonacci-memo');
let sum1 = 0;
for (let i = 0; i < 10000000; i++) {
sum1 = add(sum1, i);
}
console.timeEnd('with-function');
console.time('set-has');
const set = new Set(arrayWithDuplicates);
const hasValueInSet = set.has(500);
console.timeEnd('set-has');
String operations can be expensive, especially when dealing with large strings or
frequent concatenations.
String concatenation
String methods
const text = 'The quick brown fox jumps over the lazy dog';
// indexOf vs includes
console.time('indexOf');
const containsFox = text.indexOf('fox') !== -1;
console.timeEnd('indexOf');
console.time('includes');
const containsFoxIncludes = text.includes('fox');
console.timeEnd('includes');
// Regular expressions
console.time('regex-test');
const pattern = /fox/;
const containsFoxRegex = pattern.test(text);
console.timeEnd('regex-test');
Layout thrashing occurs when you repeatedly read and write to the DOM, forcing the
browser to recalculate layouts.
function createList(items) {
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
fragment.appendChild(li);
});
return fragment;
}
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const position = startPos + (endPos - startPos) *
progress;
element.style.transform = `translateX(${position}px)`;
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
JavaScript uses automatic garbage collection to free memory that's no longer needed.
The garbage collector identifies and removes objects that are no longer reachable from
the root (global object).
button.addEventListener('click', function() {
console.log('Button clicked', data.length);
});
}
button.addEventListener('click', handler);
// Usage
const cleanup = addCleanHandler();
// Later, when the handler is no longer needed
cleanup();
return function() {
console.log('Length:', largeData.length);
};
}
return function() {
console.log('Length:', length);
};
}
function createDetachedElements() {
const div = document.createElement('div');
div.innerHTML = 'This is a detached element';
1. Limit variable scope: Use block scope with let and const
2. Avoid global variables: They're never garbage collected while the page is loaded
3. Be careful with closures: They can inadvertently retain large objects
4. Clean up event listeners: Remove them when no longer needed
5. Use weak references: WeakMap and WeakSet allow objects to be garbage
collected
6. Dispose of large objects: Set them to null when no longer needed
7. Watch for detached DOM elements: Make sure DOM elements are properly
removed
function processUser(user) {
if (cache.has(user)) {
return cache.get(user);
}
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}
`);
}
return data;
}
add(request) {
return new Promise((resolve, reject) => {
this.queue.push({
request,
resolve,
reject
});
this.scheduleProcessing();
});
}
scheduleProcessing() {
if (this.queue.length >= this.maxBatchSize) {
// Process immediately if we've reached max batch
size
this.processQueue();
} else if (!this.timeout) {
// Otherwise, set a timeout to process soon
this.timeout = setTimeout(() => {
this.processQueue();
}, this.maxWaitTime);
}
}
async processQueue() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (this.queue.length === 0) {
return;
}
try {
// Send the batch request
const response = await fetch(this.batchUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ requests })
});
if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status}`);
}
// Usage
const batcher = new RequestBatcher('/api/batch');
// Write phase
elements.forEach((element, i) => {
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';
});
setInterval(() => {
position += 5;
element.style.left = position + 'px'; // Triggers layout
}, 16);
}
function animate() {
position += 5;
element.style.transform = `translateX(${position}
px)`; // No layout changes
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
}
return function(...args) {
clearTimeout(timeoutId);
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage
const debouncedResize = debounce(() => {
console.log('Resize event debounced');
// Expensive operation
}, 200);
window.addEventListener('resize', debouncedResize);
window.addEventListener('scroll', throttledScroll);
// main.js
function startWorker() {
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
console.log('Result from worker:', event.data);
};
worker.onerror = function(error) {
console.error('Worker error:', error);
};
// worker.js
self.onmessage = function(event) {
const { action, data } = event.data;
render(vNode, container) {
// Clear container
container.innerHTML = '';
createRealNode(vNode) {
if (typeof vNode === 'string' || typeof vNode ===
'number') {
return document.createTextNode(vNode);
}
// Create element
const element = document.createElement(type);
// Set properties
Object.entries(props || {}).forEach(([key, value]) => {
if (key === 'className') {
element.className = value;
} else if (key === 'style' && typeof value ===
'object') {
Object.entries(value).forEach(([cssKey,
cssValue]) => {
element.style[cssKey] = cssValue;
});
} else if (key.startsWith('on') && typeof value ===
'function') {
const eventName = key.slice(2).toLowerCase();
element.addEventListener(eventName, value);
} else {
element.setAttribute(key, value);
}
});
// Add children
children.forEach(child => {
element.appendChild(this.createRealNode(child));
});
return element;
}
}
// Usage
const vdom = new VirtualDOM();
vdom.render(vApp, document.getElementById('app'));
function initApp() {
// heavyFeature is loaded even if not used immediately
document.getElementById('feature-
button').addEventListener('click', () => {
heavyFeature();
});
}
// service-worker.js
const CACHE_NAME = 'my-site-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
];
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request,
responseToCache);
});
return response;
});
})
);
});
Summary
1. Create a function that measures the execution time of another function using the
Performance API.
2. Compare the performance of different loop types (for, forEach, for...of, for...in) with
arrays of various sizes.
3. Create a benchmark utility that can run multiple tests and compare their
performance.
4. Use Chrome DevTools to profile a web page and identify performance bottlenecks.
5. Implement a simple performance monitoring system that tracks key metrics over
time.
1. Create a function that efficiently adds 1000 items to a list without causing layout
thrashing.
2. Implement a virtual scrolling system that only renders visible items in a long list.
3. Create a table with sortable columns that uses event delegation for all interactions.
4. Build a simple component that efficiently updates only the parts that have
changed.
5. Implement a drag-and-drop interface that minimizes reflows and repaints.
1. Create a memory leak detector that can identify common patterns of memory
leaks.
2. Implement a cache system with automatic cleanup of old entries to prevent
memory growth.
3. Create a component that properly cleans up all resources (event listeners, timers,
etc.) when destroyed.
4. Use WeakMap and WeakSet to implement a caching system that doesn't prevent
garbage collection.
5. Create a tool that visualizes memory usage over time for a web application.
1. Implement a data compression utility that reduces the size of data sent to the
server.
2. Create a caching system for API responses with configurable expiration times.
3. Implement a request batching system that combines multiple API calls into a single
request.
4. Create a prefetching system that loads resources before they're needed.
5. Implement a retry mechanism with exponential backoff for failed network
requests.
spa-router/
├── index.html
├── js/
│ ├── app.js # Main application logic
│ ├── router.js # Router implementation
│ └── views/
│ ├── home.js # Home view module
│ ├── about.js # About view module
│ └── contact.js # Contact view module
└── css/
└── style.css
17.1.2 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<title>SPA Router Example</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<nav>
<a href="#/">Home</a>
<a href="#/about">About</a>
<a href="#/contact">Contact</a>
</nav>
<main id="app-root"></main>
17.1.3 js/router.js
// js/router.js
/**
* Sets the root element where views will be rendered.
* @param {HTMLElement} element - The root DOM element.
*/
export function setRootElement(element) {
rootElement = element;
}
/**
* Adds a route to the router.
* @param {string} path - The route path (e.g.,
/
,
/about
).
* @param {Function} viewFunction - A function that returns the
view content (HTML string or DOM element).
*/
export function addRoute(path, viewFunction) {
routes[path] = viewFunction;
}
/**
* Handles route changes based on the URL hash.
*/
async function handleRouteChange() {
if (!rootElement) {
if (viewFunction) {
try {
// Clear the current content
rootElement.innerHTML =
<p>Loading...</p>
; // Show loading state
;
if (typeof viewContent ===
string
) {
rootElement.innerHTML = viewContent;
} else if (viewContent instanceof Node) {
rootElement.appendChild(viewContent);
} else {
console.error(
Invalid view content type for path:
, path);
rootElement.innerHTML =
<p>Error loading view.</p>
;
}
} catch (error) {
console.error(
Error rendering view for path:
, path, error);
rootElement.innerHTML =
<p>Error loading view.</p>
;
}
} else {
// Handle 404 Not Found
rootElement.innerHTML =
<h1>404 - Page Not Found</h1>
;
}
}
/**
* Initializes the router by listening to hash changes and
handling the initial route.
*/
export function initializeRouter() {
// Listen for hash changes
window.addEventListener(
hashchange
, handleRouteChange);
// js/views/home.js
export default function homeView() {
return `
<h1>Welcome Home!</h1>
<p>This is the home page of our SPA.</p>
`;
}
17.1.5 js/views/about.js
// js/views/about.js
export default async function aboutView() {
// Simulate fetching data asynchronously
await new Promise(resolve => setTimeout(resolve, 500));
const data = {
title:
About Us
,
content:
We are a team dedicated to building awesome JavaScript
applications.
};
return `
<h1>${data.title}</h1>
<p>${data.content}</p>
`;
}
17.1.6 js/views/contact.js
// js/views/contact.js
export default function contactView() {
const form = document.createElement(
form
);
form.innerHTML = `
<h1>Contact Us</h1>
<label for="name">Name:</label>
<input type="text" id="name" name="name"><br>
<label for="email">Email:</label>
<input type="email" id="email" name="email"><br>
<button type="submit">Send</button>
`;
form.addEventListener(
submit
, (event) => {
event.preventDefault();
alert(
Form submitted!
);
});
17.1.7 js/app.js
// js/app.js
import { setRootElement, addRoute, initializeRouter } from
./router.js
;
import homeView from
./views/home.js
;
import aboutView from
./views/about.js
;
import contactView from
./views/contact.js
;
realtime-chart/
├── index.html
├── js/
│ ├── app.js # Main application logic
│ ├── chart.js # Chart rendering logic
│ └── websocket.js # WebSocket connection handling
└── css/
└── style.css
17.2.2 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<title>Real-time Chart</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<h1>Real-time Data</h1>
<div id="chart-container" style="width: 600px; height:
300px; border: 1px solid #ccc;">
<!-- Chart will be rendered here -->
</div>
<p>Status: <span id="status">Connecting...</span></p>
17.2.3 js/chart.js
// js/chart.js
/**
* Initializes the chart with a container element.
* @param {HTMLElement} container - The DOM element to render
the chart in.
*/
export function initializeChart(container) {
chartElement = container;
renderChart(); // Initial render
}
/**
* Adds a new data point to the chart.
* @param {number} value - The data value to add.
*/
export function addDataPoint(value) {
chartData.push(value);
renderChart();
}
/**
* Renders the chart based on the current data.
* This is a simplified simulation of chart rendering.
*/
function renderChart() {
if (!chartElement) return;
17.2.4 js/websocket.js
// js/websocket.js
/**
* Connects to the WebSocket server.
* @param {string} url - The WebSocket server URL.
* @param {Function} onData - Callback function for received
data.
* @param {Function} onStatusChange - Callback function for
status updates.
*/
export function connectWebSocket(url, onData, onStatusChange) {
onDataCallback = onData;
onStatusChangeCallback = onStatusChange;
updateStatus(
Connecting...
);
socket.onopen = () => {
updateStatus(
Connected
);
console.log(
WebSocket connection opened
);
};
/**
* Sends a message through the WebSocket.
* @param {object} message - The message object to send.
*/
export function sendMessage(message) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
} else {
console.error(
WebSocket is not connected.
);
}
}
/**
* Closes the WebSocket connection.
*/
export function closeWebSocket() {
if (socket) {
socket.close();
}
}
/**
* Updates the connection status and calls the callback.
* @param {string} status - The new status message.
*/
function updateStatus(status) {
if (onStatusChangeCallback) {
onStatusChangeCallback(status);
}
}
17.2.5 js/app.js
// js/app.js
import { initializeChart, addDataPoint } from
./chart.js
;
import { connectWebSocket } from
./websocket.js
;
function handleStatusChange(status) {
statusElement.textContent = status;
}
connectWebSocket(WEBSOCKET_URL, handleNewData,
handleStatusChange);
offline-notes/
├── index.html
├── manifest.json # Web App Manifest
├── service-worker.js # Service Worker script
├── js/
│ ├── app.js # Main application logic
│ ├── db.js # IndexedDB helper module
│ └── ui.js # UI update functions
├── css/
│ └── style.css
└── icons/
└── icon-192.png # App icon
17.3.2 manifest.json
{
"name": "Offline Notes App",
"short_name": "Notes",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3367D6",
"icons": [
{
"src": "icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
17.3.3 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-
scale=1.0">
<title>Offline Notes</title>
<link rel="stylesheet" href="css/style.css">
<link rel="manifest" href="manifest.json">
</head>
<body>
<h1>Offline Notes</h1>
<form id="note-form">
<textarea id="note-input" placeholder="Enter your
note..."></textarea>
<button type="submit">Add Note</button>
</form>
<ul id="notes-list"></ul>
// js/db.js
const DB_NAME =
NotesDB
;
const DB_VERSION = 1;
const STORE_NAME =
notes
;
let db;
/**
* Opens the IndexedDB database.
* @returns {Promise<IDBDatabase>} A promise that resolves with
the database instance.
*/
function openDB() {
return new Promise((resolve, reject) => {
if (db) {
resolve(db);
return;
}
/**
* Adds a note to the database.
* @param {object} note - The note object (e.g., { text:
...
, timestamp: ... }).
* @returns {Promise<number>} A promise that resolves with the
ID of the added note.
*/
export async function addNote(note) {
const dbInstance = await openDB();
return new Promise((resolve, reject) => {
const transaction =
dbInstance.transaction([STORE_NAME],
readwrite
);
const store = transaction.objectStore(STORE_NAME);
const request = store.add(note);
/**
* Retrieves all notes from the database.
* @returns {Promise<Array<object>>} A promise that resolves
with an array of notes.
*/
export async function getAllNotes() {
const dbInstance = await openDB();
return new Promise((resolve, reject) => {
const transaction =
dbInstance.transaction([STORE_NAME],
readonly
);
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
/**
* Deletes a note from the database.
* @param {number} id - The ID of the note to delete.
* @returns {Promise<void>} A promise that resolves when the
note is deleted.
*/
export async function deleteNote(id) {
const dbInstance = await openDB();
return new Promise((resolve, reject) => {
const transaction =
dbInstance.transaction([STORE_NAME],
readwrite
);
const store = transaction.objectStore(STORE_NAME);
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
17.3.5 js/ui.js
// js/ui.js
import { deleteNote } from
./db.js
;
/**
* Renders the list of notes in the UI.
* @param {Array<object>} notes - An array of note objects.
*/
export function renderNotes(notes) {
notesListElement.innerHTML =
if (notes.length === 0) {
notesListElement.innerHTML =
<li>No notes yet.</li>
;
return;
}
notes.forEach(note => {
const li = document.createElement(
li
);
li.textContent = note.text;
li.dataset.id = note.id;
li.appendChild(deleteButton);
notesListElement.appendChild(li);
});
}
/**
* Initializes UI event listeners.
* @param {Function} loadNotesCallback - Callback to reload
notes after deletion.
*/
export function initializeUI(loadNotesCallback) {
notesListElement.addEventListener(
click
, async (event) => {
if (event.target.classList.contains(
delete-btn
)) {
const noteId = parseInt(event.target.closest(
li
).dataset.id, 10);
if (!isNaN(noteId)) {
try {
await deleteNote(noteId);
loadNotesCallback(); // Reload notes after
deletion
} catch (error) {
console.error(
Failed to delete note:
, error);
alert(
Failed to delete note.
);
}
}
}
});
}
17.3.6 js/app.js
// js/app.js
import { addNote, getAllNotes } from
./db.js
;
import { renderNotes, initializeUI } from
./ui.js
;
/**
* Loads notes from the database and renders them.
*/
async function loadAndRenderNotes() {
try {
const notes = await getAllNotes();
renderNotes(notes);
} catch (error) {
console.error(
Failed to load notes:
, error);
alert(
Failed to load notes.
);
}
}
/**
* Handles the form submission to add a new note.
* @param {Event} event - The form submission event.
*/
async function handleAddNote(event) {
event.preventDefault();
const text = noteInput.value.trim();
if (text) {
const newNote = {
text: text,
timestamp: new Date().toISOString()
};
try {
await addNote(newNote);
noteInput.value =
; // Clear input
loadAndRenderNotes(); // Refresh the list
} catch (error) {
console.error(
Failed to add note:
, error);
alert(
Failed to add note.
);
}
}
}
/**
* Registers the Service Worker.
*/
function registerServiceWorker() {
if (
serviceWorker
in navigator) {
window.addEventListener(
load
, () => {
navigator.serviceWorker.register(
/service-worker.js
)
.then(registration => {
console.log(
ServiceWorker registration successful with scope:
, registration.scope);
})
.catch(error => {
console.error(
ServiceWorker registration failed:
, error);
});
});
}
}
17.3.7 service-worker.js
// service-worker.js
const CACHE_NAME =
offline-notes-cache-v1
;
const urlsToCache = [
/
,
/index.html
,
/css/style.css
,
/js/app.js
,
/js/db.js
,
/js/ui.js
,
/manifest.json
,
/icons/icon-192.png
];
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return response
if (response) {
console.log(
[Service Worker] Serving from cache:
, event.request.url);
return response;
}
As you continue your JavaScript journey, practice integrating these concepts into your
own projects. Experiment, break things, and learn from the process. The ability to
effectively combine these advanced techniques is key to becoming a proficient
JavaScript developer.
Summary
• SPA Router: Showcased ES6 modules, async/await, DOM manipulation, and hash-
based routing.
• Real-time Chart: Illustrated WebSockets, event-driven programming, and basic
data visualization.
• Offline Notes App: Demonstrated Service Workers, IndexedDB, PWA principles,
and an offline-first approach.
Exercises
1. Replace the simulated chart rendering with a proper charting library (e.g., Chart.js,
Plotly.js).
2. Implement WebSocket reconnection logic with exponential backoff.
3. Add user controls to pause/resume the real-time updates.
4. Allow users to configure the WebSocket URL.
5. Visualize multiple data streams on the same chart.
1. Create a data grid component that can handle 10,000+ rows efficiently.
2. Implement virtual scrolling (only rendering visible rows).
3. Use Web Workers for sorting and filtering large datasets.
4. Optimize DOM updates to minimize reflows/repaints.
5. Benchmark the component and identify areas for further optimization.
return total;
}
Whether you choose to use semicolons or not, be consistent throughout your codebase.
// With semicolons
const name = 'John';
const greeting = `Hello, ${name}`;
console.log(greeting);
return total;
}
// Some code...
// More code...
let total = subtotal * (1 + taxRate);
return total;
}
function updateUser() {
userId = 43;
userName = 'Jane';
}
function getUserInfo() {
return { id: userId, name: userName };
}
return {
updateUser,
getUserInfo
};
})();
A.3 Functions
// Calculate subtotal
let subtotal = 0;
for (const item of order.items) {
subtotal += item.price * item.quantity;
}
// Apply discounts
if (order.discountCode === 'SAVE10') {
subtotal *= 0.9;
}
// Calculate tax
const tax = subtotal * 0.07;
// Calculate total
const total = subtotal + tax;
// Update inventory
for (const item of order.items) {
updateInventory(item.id, item.quantity);
}
// Return result
return {
subtotal,
tax,
total
};
}
function calculateSubtotal(items) {
return items.reduce((sum, item) => sum + item.price *
item.quantity, 0);
}
function calculateTax(amount) {
return amount * 0.07;
}
function updateInventoryForOrder(items) {
for (const item of items) {
updateInventory(item.id, item.quantity);
}
}
function processOrder(order) {
validateOrder(order);
updateInventoryForOrder(order.items);
// Usage
const user1 = createUser('John'); // role = 'user', active =
true
const user2 = createUser('Jane', 'admin'); // active = true
const user3 = createUser('Bob', 'editor', false);
A.3.3 Return Early
if (!user.active) {
return [];
}
return ['read'];
}
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
if (!response.ok) {
throw new Error(`HTTP error! Status: $
{response.status}`);
}
// Usage
function validateUser(user) {
if (!user.name) {
throw new ValidationError('Name is required');
}
if (!user.email) {
throw new ValidationError('Email is required');
}
}
try {
validateUser({ name: 'John' });
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation failed:', error.message);
} else {
console.error('Unexpected error:', error);
}
}
A.7 Performance
// Debounce function
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
// Usage
const searchInput = document.getElementById('search');
// Main thread
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
console.log('Result:', event.data);
};
worker.postMessage({
numbers: Array.from({ length: 10000000 }, (_, i) => i)
});
// worker.js
self.onmessage = function(event) {
const { numbers } = event.data;
// CPU-intensive task
const sum = numbers.reduce((total, num) => total + num, 0);
self.postMessage(sum);
};
A.8 Security
if (!allowedPattern.test(input)) {
throw new Error('Invalid input');
}
A.9 Testing
// Usage
const process = processData(
() => fetchDataFromServer(),
(data) => saveToDatabase(data)
);
// Test
const mockFetch = () => Promise.resolve([{ value: 1 }, { value:
2 }]);
const mockSave = jest.fn();
const testProcess = processData(mockFetch, mockSave);
testProcess().then(() => {
expect(mockSave).toHaveBeenCalledWith([2, 4]);
});
if (b === 0) {
throw new Error('Cannot divide by zero');
}
return a / b;
}
A.10 Documentation
/**
* Calculates the monthly payment for a loan.
*
* @param {number} principal - The loan amount
* @param {number} interestRate - Annual interest rate (decimal)
* @param {number} years - Loan term in years
* @returns {number} Monthly payment amount
*
* @example
* // Returns 1687.71
* calculateMonthlyPayment(200000, 0.04, 15);
*/
function calculateMonthlyPayment(principal, interestRate,
years) {
const monthlyRate = interestRate / 12;
const payments = years * 12;
return (
principal *
monthlyRate *
Math.pow(1 + monthlyRate, payments) /
(Math.pow(1 + monthlyRate, payments) - 1)
).toFixed(2);
}
/**
* Formats a date as a string.
*
* @param {Date} date - The date to format
* @param {string} [format='short'] - The format to use:
* 'short' (MM/DD/YYYY)
* 'long' (Month DD, YYYY)
* 'iso' (YYYY-MM-DD)
* @returns {string} The formatted date string
*
* @example
* const date = new Date('2023-05-15');
*
* // Returns "05/15/2023"
* formatDate(date);
*
* // Returns "May 15, 2023"
* formatDate(date, 'long');
*
* // Returns "2023-05-15"
* formatDate(date, 'iso');
*/
function formatDate(date, format = 'short') {
// Implementation
}
A.11 Conclusion
Following these best practices will help you write cleaner, more maintainable, and more
efficient JavaScript code. Remember that best practices evolve over time, so it's
important to stay up-to-date with the latest recommendations and adapt your coding
style accordingly.
As you gain experience, you'll develop a sense of when to strictly follow these guidelines
and when it might be appropriate to deviate from them based on specific project
requirements or constraints. The most important thing is to be consistent and
thoughtful in your approach to writing JavaScript code.
Syntax errors occur when your code violates JavaScript's grammar rules. These errors
prevent your code from running at all.
Reference errors occur when you try to use a variable or function that doesn't exist in the
current scope.
// Undefined variable
console.log(username); // ReferenceError: username is not
defined
Type errors occur when an operation is performed on a value of the wrong type.
// Calling a non-function
const user = { name: "John" };
user.getName(); // TypeError: user.getName is not a function
Logic errors don't produce error messages but cause your program to behave
incorrectly. These can be the hardest to find.
// Infinite loop
for (let i = 0; i < 10; i--) {
console.log(i); // Will run forever
}
// Incorrect comparison
if (userRole = "admin") { // Assignment instead of comparison
(===)
grantAdminAccess();
}
// Off-by-one error
const months = ["Jan", "Feb", "Mar", "Apr", "May"];
for (let i = 0; i <= months.length; i++) { // Should be < not <=
console.log(months[i]); // Last iteration gives undefined
}
B.2.1 console.log()
return total;
}
B.2.2 console.table()
const users = [
{ id: 1, name: "John", role: "admin" },
{ id: 2, name: "Jane", role: "editor" },
{ id: 3, name: "Bob", role: "user" }
];
console.table(users);
B.2.3 console.dir()
B.2.4 console.trace()
function function1() {
function2();
}
function function2() {
function3();
}
function function3() {
console.trace(); // Shows the call stack
}
function1();
console.time('arrayProcessing');
B.2.6 console.assert()
function processPayment(amount) {
console.assert(amount > 0, 'Payment amount must be
positive');
// Process payment...
}
processPayment(-10); // Assertion failed: Payment amount must be
positive
The debugger statement pauses execution and opens the browser's debugging tools.
When execution reaches that line, it will pause, allowing you to: - Inspect variable values
- Step through code line by line - See the call stack - Evaluate expressions in the console
In browser DevTools, you can set breakpoints that only trigger when a condition is met:
You can also set breakpoints that trigger when specific network requests are made:
The call stack shows the path of execution that led to the current point:
You can set breakpoints that trigger when the DOM is modified:
Source maps allow you to debug transpiled or minified code as if it were the original
source code.
// Stop monitoring
unmonitorEvents(button);
When debugging loops, it's often helpful to log the iteration number and relevant values:
// Process item...
To debug event handlers, log both the event and relevant state:
document.getElementById('submit-
form').addEventListener('submit', function(event) {
console.log('Form submitted', {
event,
formData: new FormData(event.target),
timestamp: new Date()
});
// Process form...
});
try {
const response = await fetch(url);
console.log('Response status:', response.status);
return data;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
B.6.4 Isolating the Problem
function complexFunction() {
// Part 1
// ...
// Part 2
// ...
// Part 3
// ...
}
// Debugging approach:
function complexFunction() {
// Part 1
// ...
// Part 2
// Comment out to see if the issue is in Part 2
// ...
// Part 3
// ...
}
logApp('Application starting');
logAPI('Fetching user data');
logUI('Rendering dashboard');
Sentry.init({
dsn: 'https://[email protected]/0',
environment: process.env.NODE_ENV
});
try {
riskyOperation();
} catch (error) {
Sentry.captureException(error);
// Show user-friendly error message
}
B.8 Debugging Best Practices
By mastering these techniques, you'll be able to find and fix bugs more efficiently,
leading to more robust and reliable JavaScript applications.
• MDN Web Docs (Mozilla Developer Network) - The most comprehensive and
reliable reference for JavaScript.
• ECMAScript Specification - The official JavaScript language specification.
• TC39 Proposals - Track upcoming JavaScript features.
C.3.1 Blogs
C.3.2 Newsletters
C.6 Books
C.8 Podcasts
• JavaScript Jabber
• Syntax
• JS Party
• The Frontend Podcast
• React Podcast
• JSConf
• NodeConf
• React Conf
• VueConf
• SmashingConf
C.10 Coding Challenges and Practice
Regular practice is key to mastering JavaScript:
C.13 Conclusion
The resources listed in this appendix provide a solid foundation for continuing your
JavaScript learning journey. Remember that the best way to learn is by doing—build
projects, solve problems, and contribute to the community. As you grow as a developer,
you'll discover which resources work best for your learning style and goals.
Happy coding!